<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: NuvyntraLabs</title>
    <description>The latest articles on DEV Community by NuvyntraLabs (@nuvyantralabs_ccee).</description>
    <link>https://dev.to/nuvyantralabs_ccee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4138260%2Fe149499a-8204-4e2a-b191-6a06912983c9.png</url>
      <title>DEV Community: NuvyntraLabs</title>
      <link>https://dev.to/nuvyantralabs_ccee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nuvyantralabs_ccee"/>
    <language>en</language>
    <item>
      <title>NuvLoc 1.1.2: The Agent Writes the Culture .resx. The CLI Checks It.</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:05:48 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/nuvloc-112-the-agent-writes-the-culture-resx-the-cli-checks-it-1fe7</link>
      <guid>https://dev.to/nuvyantralabs_ccee/nuvloc-112-the-agent-writes-the-culture-resx-the-cli-checks-it-1fe7</guid>
      <description>&lt;p&gt;A translation vendor wants an API key. An IDE resource manager wants you to stay in that IDE. A MAUI app that already has an English &lt;code&gt;.resx&lt;/code&gt; mostly needs to know which keys are missing in Spanish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuvLoc&lt;/strong&gt; answers that question and hands the missing keys to the coding agent you already use. Version &lt;strong&gt;1.1.2&lt;/strong&gt; of &lt;code&gt;NuvyntraLabs.NuvLoc.Cli&lt;/code&gt; is on nuget.org. The agent writes &lt;code&gt;AppResources.es.resx&lt;/code&gt;. The CLI never calls OpenAI, Azure, or any other translation API.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough: &lt;code&gt;i18n.json&lt;/code&gt;, &lt;code&gt;nuvloc init&lt;/code&gt;, the two slash commands, and the CI check that does not need an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What NuvLoc is
&lt;/h2&gt;

&lt;p&gt;NuvLoc is a global &lt;code&gt;dotnet&lt;/code&gt; tool for sibling &lt;code&gt;.resx&lt;/code&gt; hosts: MAUI, WPF, WinUI, Avalonia, and Uno. MIT licensed. The command is &lt;code&gt;nuvloc&lt;/code&gt;. It is not a &lt;code&gt;PackageReference&lt;/code&gt;, and it does not bind strings into XAML.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Package&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.nuget.org/packages/NuvyntraLabs.NuvLoc.Cli" rel="noopener noreferrer"&gt;&lt;code&gt;NuvyntraLabs.NuvLoc.Cli&lt;/code&gt;&lt;/a&gt; 1.1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;i18n.json&lt;/code&gt; at the project root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/nuvloc.translate&lt;/code&gt; — there is no &lt;code&gt;nuvloc translate&lt;/code&gt; command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nuvloc check --ci&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/nuvyntralabs/NuvLoc" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/NuvLoc&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/toolkits/nuvloc/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/toolkits/nuvloc/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Coverage and placeholder checks prove that every key is present and that &lt;code&gt;{0}&lt;/code&gt; still matches. They do not prove the sentence is right. Review every culture file with a native speaker before you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and configure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; NuvyntraLabs.NuvLoc.Cli &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json
nuvloc version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put &lt;code&gt;i18n.json&lt;/code&gt; next to the project. &lt;code&gt;init&lt;/code&gt; checks this file. It does not create the host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"maui"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Resources/Strings/AppResources.resx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"languages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"es"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fr"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;platform&lt;/code&gt; is &lt;code&gt;maui&lt;/code&gt;, &lt;code&gt;wpf&lt;/code&gt;, &lt;code&gt;winui&lt;/code&gt;, &lt;code&gt;avalonia&lt;/code&gt;, or &lt;code&gt;uno&lt;/code&gt;. &lt;code&gt;source&lt;/code&gt; is the English &lt;code&gt;.resx&lt;/code&gt;. Culture files are siblings: &lt;code&gt;AppResources.es.resx&lt;/code&gt; beside &lt;code&gt;AppResources.resx&lt;/code&gt;. WinUI and Uno PRI &lt;code&gt;.resw&lt;/code&gt; folders under &lt;code&gt;Strings/{lang}/&lt;/code&gt; stay out of scope.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;languages&lt;/code&gt; must be BCP-47. &lt;code&gt;es&lt;/code&gt;, &lt;code&gt;pt-BR&lt;/code&gt;, and &lt;code&gt;zh-Hans&lt;/code&gt; are valid. Casing is canonicalized (&lt;code&gt;PT-br&lt;/code&gt; becomes &lt;code&gt;pt-BR&lt;/code&gt;). Underscores (&lt;code&gt;es_MX&lt;/code&gt;), and English source tags (&lt;code&gt;en&lt;/code&gt;, &lt;code&gt;en-US&lt;/code&gt;, &lt;code&gt;en-GB&lt;/code&gt;), are rejected. A bad code does not create a &lt;code&gt;.resx&lt;/code&gt;. The CLI exits 2 and prints the reason.&lt;/p&gt;

&lt;p&gt;These codes are tested: &lt;code&gt;es&lt;/code&gt;, &lt;code&gt;fr&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, &lt;code&gt;it&lt;/code&gt;, &lt;code&gt;nl&lt;/code&gt;, &lt;code&gt;ja&lt;/code&gt;, &lt;code&gt;ko&lt;/code&gt;, &lt;code&gt;zh-Hans&lt;/code&gt;, &lt;code&gt;pt-BR&lt;/code&gt;, &lt;code&gt;ar&lt;/code&gt;, &lt;code&gt;hi&lt;/code&gt;, &lt;code&gt;ru&lt;/code&gt;. Other BCP-47 codes work the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Init, then the agent
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nuvloc init &lt;span class="nt"&gt;--configfile&lt;/span&gt; i18n.json &lt;span class="nt"&gt;--agent&lt;/span&gt; cursor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; checks the JSON, confirms the source file exists, and writes &lt;code&gt;.nuvloc/&lt;/code&gt; plus the agent skills. &lt;code&gt;--force&lt;/code&gt; overwrites skill files. It does not overwrite a culture &lt;code&gt;.resx&lt;/code&gt;. Omit &lt;code&gt;--agent&lt;/code&gt; on an interactive terminal to pick from the same Spec Kit set Nuvyn uses (Cursor, GitHub Copilot, Claude Code, Gemini CLI, Codex, Windsurf, and others). &lt;code&gt;--ci&lt;/code&gt; requires &lt;code&gt;--agent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Cursor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/nuvloc.status
/nuvloc.translate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/nuvloc.status&lt;/code&gt; runs &lt;code&gt;nuvloc status&lt;/code&gt; and explains the counts. &lt;code&gt;/nuvloc.translate&lt;/code&gt; runs &lt;code&gt;nuvloc plan&lt;/code&gt;, writes the sibling culture files, then runs &lt;code&gt;nuvloc check --write-cache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A plan with gaps looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source: Resources/Strings/AppResources.resx

es: 2 current, 1 missing, 0 stale, 0 placeholder, 0 extra
 missing ItemsLeft

fr: 0 current, 3 missing, 0 stale, 0 placeholder, 0 extra
 missing Save
 missing Cancel
 missing ItemsLeft
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;current&lt;/code&gt; means the key is present and the source hash still matches. &lt;code&gt;stale&lt;/code&gt; means the English value changed after the culture file was written. &lt;code&gt;placeholder&lt;/code&gt; means a format item does not match. &lt;code&gt;extra&lt;/code&gt; means the culture file has a key the English source does not.&lt;/p&gt;

&lt;p&gt;A second agent is &lt;code&gt;nuvloc agent add copilot&lt;/code&gt;. &lt;code&gt;nuvloc update&lt;/code&gt; refreshes skills and &lt;code&gt;.nuvloc/reference/dotnet-resx.md&lt;/code&gt; and leaves &lt;code&gt;i18n.json&lt;/code&gt; and the culture files alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI without an agent
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nuvloc check &lt;span class="nt"&gt;--configfile&lt;/span&gt; i18n.json &lt;span class="nt"&gt;--ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit &lt;code&gt;0&lt;/code&gt; means the configured cultures are complete. Exit &lt;code&gt;1&lt;/code&gt; means a key is missing, stale, or a placeholder is broken. Exit &lt;code&gt;2&lt;/code&gt; is usage: missing &lt;code&gt;i18n.json&lt;/code&gt;, an unknown agent, a language that is not BCP-47, or &lt;code&gt;--lang&lt;/code&gt; that is not listed in &lt;code&gt;i18n.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--write-cache&lt;/code&gt; records source hashes for keys that are present, so the next run can tell stale from current. &lt;code&gt;--ci&lt;/code&gt; forces JSON.&lt;/p&gt;

&lt;p&gt;On an interactive terminal &lt;code&gt;nuvloc&lt;/code&gt; asks every 4 hours whether to update from nuget.org. The cache file is &lt;code&gt;~/.nuvyntra/cli-updates.json&lt;/code&gt;, shared with &lt;code&gt;maui-dev&lt;/code&gt;, &lt;code&gt;nuvyn&lt;/code&gt;, &lt;code&gt;maui-perf&lt;/code&gt;, and &lt;code&gt;maui-pulse&lt;/code&gt;. Skip it with &lt;code&gt;--no-update-check&lt;/code&gt; or &lt;code&gt;NUVYNTRA_NO_UPDATE_CHECK=1&lt;/code&gt;. The CLI does not phone home.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exit 2 on &lt;code&gt;es_MX&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Use a hyphen: &lt;code&gt;es-MX&lt;/code&gt;. Underscores are rejected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit 2 on &lt;code&gt;en-US&lt;/code&gt; in &lt;code&gt;languages&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;English is the &lt;code&gt;source&lt;/code&gt; file. Do not list it as a target culture.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/nuvloc.translate&lt;/code&gt; is missing&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;nuvloc init --agent cursor&lt;/code&gt; in this repo. &lt;code&gt;init&lt;/code&gt; installs the skill.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nuvloc translate&lt;/code&gt; fails&lt;/td&gt;
&lt;td&gt;That command does not exist. Translation is the slash skill.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check is green and the Spanish is wrong&lt;/td&gt;
&lt;td&gt;Completeness passed. A native speaker still has to read the file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You expected XAML to pick up the new culture&lt;/td&gt;
&lt;td&gt;NuvLoc does not add a markup extension. The host binds &lt;code&gt;AppResources&lt;/code&gt; the way it already does.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where NuvLoc sits next to the other tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Diff sibling &lt;code&gt;.resx&lt;/code&gt; files and let the agent fill the gaps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NuvLoc&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An IDE resource editor or a translation vendor&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/dotnet/ResXResourceManager" rel="noopener noreferrer"&gt;ResXResourceManager&lt;/a&gt;, Crowdin, or Phrase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doctor the MAUI project&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/toolkits/maui-dev/" rel="noopener noreferrer"&gt;MauiDev&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaffold a new MAUI host&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/toolkits/nuvyn/" rel="noopener noreferrer"&gt;Nuvyn&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; NuvyntraLabs.NuvLoc.Cli &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json
nuvloc init &lt;span class="nt"&gt;--configfile&lt;/span&gt; i18n.json &lt;span class="nt"&gt;--agent&lt;/span&gt; cursor
nuvloc status &lt;span class="nt"&gt;--configfile&lt;/span&gt; i18n.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;/nuvloc.translate&lt;/code&gt;, and have a native speaker read &lt;code&gt;AppResources.es.resx&lt;/code&gt; before the check in CI is the only review it gets.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/NuvyntraLabs.NuvLoc.Cli" rel="noopener noreferrer"&gt;NuvyntraLabs.NuvLoc.Cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/nuvyntralabs/NuvLoc" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/NuvLoc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sample: &lt;a href="https://github.com/nuvyntralabs/NuvLoc/tree/main/samples/NuvLocSample" rel="noopener noreferrer"&gt;samples/NuvLocSample&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Toolkit page: &lt;a href="https://nuvyntralabs.github.io/toolkits/nuvloc/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/toolkits/nuvloc/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>maui</category>
      <category>localization</category>
      <category>i18n</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>Pulse 1.0.1: Nine Lanes for the Plugins the App Already Registered</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:05:23 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/pulse-101-nine-lanes-for-the-plugins-the-app-already-registered-1m5p</link>
      <guid>https://dev.to/nuvyantralabs_ccee/pulse-101-nine-lanes-for-the-plugins-the-app-already-registered-1m5p</guid>
      <description>&lt;p&gt;A field app fails in the yard and the log is a wall of framework noise. The useful line is the one the app already raised: captive portal, a failed job, a sync conflict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pulse&lt;/strong&gt; shows those lines in a nine-lane table while the session is still running. Version &lt;strong&gt;1.0.1&lt;/strong&gt; ships as two packages at the same version. The host listens. The CLI draws.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough: &lt;code&gt;UseMauiPulse()&lt;/code&gt;, &lt;code&gt;maui-pulse attach&lt;/code&gt;, and how to read a quiet row.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Pulse is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Plugin.Maui.Pulse&lt;/code&gt; is a Debug sink inside the MAUI app. &lt;code&gt;UseMauiPulse()&lt;/code&gt; finds allow-listed plugins that are already in dependency injection, stays subscribed to their events, and POSTs JSON to the development machine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Plugin.Maui.Pulse.Cli&lt;/code&gt; is the global tool. &lt;code&gt;maui-pulse attach&lt;/code&gt; listens on port 7878 and draws the table. The person using the app does not tap anything for Pulse.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.nuget.org/packages/Plugin.Maui.Pulse" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.Pulse&lt;/code&gt;&lt;/a&gt; 1.0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.nuget.org/packages/Plugin.Maui.Pulse.Cli" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.Pulse.Cli&lt;/code&gt;&lt;/a&gt; 1.0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command&lt;/td&gt;
&lt;td&gt;&lt;code&gt;maui-pulse attach&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;http://127.0.0.1:7878/&lt;/code&gt; (emulator: &lt;code&gt;10.0.2.2&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/nuvyntralabs/MauiPulse" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/MauiPulse&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/toolkits/maui-pulse/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/toolkits/maui-pulse/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pulse does not scrape logcat, Charles, Firebase, Sentry, or MAUI Connectivity. Unknown sources are dropped. It does not start a plugin, and it does not invent a row from the operating system when a package is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Register the host
&lt;/h2&gt;

&lt;p&gt;Add the host package to the app. Install the CLI on the machine. Do not add &lt;code&gt;Plugin.Maui.Pulse.Cli&lt;/code&gt; as a &lt;code&gt;PackageReference&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.Pulse &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.1
dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; Plugin.Maui.Pulse.Cli &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMauiApp&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseMauiPulse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UseMauiPulse()&lt;/code&gt; only registers services. After &lt;code&gt;Build()&lt;/code&gt;, the initializer resolves the package name (&lt;code&gt;AppInfo.PackageName&lt;/code&gt; unless you set &lt;code&gt;Package&lt;/code&gt;), opens the sink, walks the allow-list, and subscribes to instances already in DI. A plugin registered after that pass is missed.&lt;/p&gt;

&lt;p&gt;Release builds set &lt;code&gt;Enabled&lt;/code&gt; to false before your callback. Set &lt;code&gt;options.Enabled = true&lt;/code&gt; only when you intend the sink to run in Release. A closed CLI does not crash the host: the POST times out in 3 seconds and the error is swallowed.&lt;/p&gt;

&lt;p&gt;The app still registers the plugins it already uses. Pulse only listens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach
&lt;/h2&gt;

&lt;p&gt;USB Android needs a reverse before the phone can reach the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb reverse tcp:7878 tcp:7878
maui-pulse attach &lt;span class="nt"&gt;--package&lt;/span&gt; com.myapp.android &lt;span class="nt"&gt;--android&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 7878
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maui-pulse attach &lt;span class="nt"&gt;--package&lt;/span&gt; com.myapp.ios &lt;span class="nt"&gt;--ios&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 7878
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every session command requires &lt;code&gt;--package&lt;/code&gt; (one Android application id or one iOS bundle id). &lt;code&gt;attach&lt;/code&gt; and &lt;code&gt;listen&lt;/code&gt; also require &lt;code&gt;--android&lt;/code&gt; or &lt;code&gt;--ios&lt;/code&gt;. JSON for a different package is dropped. Two apps need two Pulse windows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;maui-pulse 1.0.1  attach  com.myapp.android  android  :7878
session 8f2a

NETWORK   Plugin.Maui.NetworkMonitor      captive portal
API       Plugin.Maui.NetworkDiagnostics  —
QUEUE     Plugin.Maui.JobQueue            visits#184
          Plugin.Maui.RetryQueue          — not installed
SYNC      Plugin.Maui.OfflineSync         conflict on Visit
PERMS     Plugin.Maui.PermissionFlow      —
HEALTH    Plugin.Maui.AppHealth           battery 18%
LEAK      Plugin.Maui.LeakAnalyser        —
CRASH     Plugin.Maui.Diagnostics         —
SESSION   Plugin.Maui.DeviceSession       session 8f2a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Row&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A headline (&lt;code&gt;captive portal&lt;/code&gt;, &lt;code&gt;visits#184&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;At least one allow-listed event arrived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;—&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subscribed, waiting. This is not a failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;— not installed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The assembly is not loaded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;— not registered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The package is referenced and &lt;code&gt;UseX()&lt;/code&gt; / &lt;code&gt;AddX()&lt;/code&gt; was not called&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;--lanes network,sync,queue&lt;/code&gt; hides rows. It does not add plugins to the allow-list. GeoLocator, PushRouter, SmartUpload, and every other package are never a lane.&lt;/p&gt;

&lt;p&gt;The nine lanes, and the registration Pulse looks for:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lane&lt;/th&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Register&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NETWORK&lt;/td&gt;
&lt;td&gt;NetworkMonitor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AddNetworkMonitor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;NetworkDiagnostics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseNetworkDiagnostics&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QUEUE&lt;/td&gt;
&lt;td&gt;JobQueue, and RetryQueue on its own row&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UseMauiJobQueue&lt;/code&gt;, &lt;code&gt;UseMauiRetryQueue&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SYNC&lt;/td&gt;
&lt;td&gt;OfflineSync&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseOfflineSync&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PERMS&lt;/td&gt;
&lt;td&gt;PermissionFlow&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UsePermissionFlow&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEALTH&lt;/td&gt;
&lt;td&gt;AppHealth&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseAppHealth&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LEAK&lt;/td&gt;
&lt;td&gt;LeakAnalyser&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseLeakAnalyser&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRASH&lt;/td&gt;
&lt;td&gt;Diagnostics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseMauiDiagnostics&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SESSION&lt;/td&gt;
&lt;td&gt;DeviceSession&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseDeviceSession&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;QUEUE is two plugins. JobQueue alone is a valid session. RetryQueue then shows &lt;code&gt;— not installed&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  After the session
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;maui-pulse listen&lt;/code&gt; is the same HTTP sink with no table. &lt;code&gt;GET /&lt;/code&gt; returns &lt;code&gt;{ "ok": true, "tool": "maui-pulse" }&lt;/code&gt;. &lt;code&gt;--stdin&lt;/code&gt; accepts one JSON payload for a test or an agent.&lt;/p&gt;

&lt;p&gt;Live events are optional for the file lanes. Copy the known files off the device, then inspect them. &lt;code&gt;pull&lt;/code&gt; copies from a folder you already have. &lt;code&gt;--adb&lt;/code&gt; only prints how to copy. It does not scrape logcat, and &lt;code&gt;logcat.txt&lt;/code&gt; in that folder is ignored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maui-pulse pull &lt;span class="nt"&gt;--package&lt;/span&gt; com.myapp.android &lt;span class="nt"&gt;--from&lt;/span&gt; ./device-files &lt;span class="nt"&gt;--out&lt;/span&gt; ./pulled
maui-pulse queues &lt;span class="nt"&gt;--package&lt;/span&gt; com.myapp.android &lt;span class="nt"&gt;--from&lt;/span&gt; ./pulled
maui-pulse &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; com.myapp.android &lt;span class="nt"&gt;--from&lt;/span&gt; ./pulled
maui-pulse incident &lt;span class="nt"&gt;--package&lt;/span&gt; com.myapp.android &lt;span class="nt"&gt;--from&lt;/span&gt; ./pulled &lt;span class="nt"&gt;--out&lt;/span&gt; incident.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Known names: &lt;code&gt;plugin.maui.jobqueue.db3&lt;/code&gt;, &lt;code&gt;plugin.maui.retryqueue.db3&lt;/code&gt;, &lt;code&gt;offlinesync.db3&lt;/code&gt;, and &lt;code&gt;maui-diagnostics/&lt;/code&gt;. &lt;code&gt;queues&lt;/code&gt; and &lt;code&gt;sync&lt;/code&gt; are read-only. They do not drain a queue or retry a row. &lt;code&gt;incident&lt;/code&gt; zips the allow-listed files plus &lt;code&gt;manifest.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Exit &lt;code&gt;0&lt;/code&gt; includes skipped lanes and missing queue files. Exit &lt;code&gt;1&lt;/code&gt; means no allow-listed evidence or the port bind failed. Exit &lt;code&gt;2&lt;/code&gt; is usage.&lt;/p&gt;

&lt;p&gt;On an interactive terminal the CLI asks every 4 hours whether to update from nuget.org. Skip it with &lt;code&gt;--no-update-check&lt;/code&gt; or &lt;code&gt;NUVYNTRA_NO_UPDATE_CHECK=1&lt;/code&gt;. CI, &lt;code&gt;--format json&lt;/code&gt;, and piped output skip the prompt. The CLI does not phone home.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every lane is &lt;code&gt;— not installed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Those packages are not in this app. Pulse will not fill the lane from the OS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;— not registered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The assembly loaded. Call the plugin's &lt;code&gt;UseX()&lt;/code&gt; before the app finishes starting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The table stays empty and the phone is on USB&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;adb reverse tcp:7878 tcp:7878&lt;/code&gt; again after unplug.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A plugin you added at runtime never appears&lt;/td&gt;
&lt;td&gt;Bind runs once at startup. Register it in &lt;code&gt;MauiProgram&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release is silent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Enabled&lt;/code&gt; is false unless you set &lt;code&gt;options.Enabled = true&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry or logcat lines never show up&lt;/td&gt;
&lt;td&gt;They are dropped. Pulse renders the nine lanes only.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-observability/" rel="noopener noreferrer"&gt;Plugin.Maui.Observability&lt;/a&gt; can POST at the same port. It is a pipe, not a tenth lane. Domains other than Network, Health, Sync, and Session are dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Pulse sits next to the other tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What the allow-listed plugins are doing in this Debug session&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pulse&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDK, workloads, and the csproj&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/toolkits/maui-dev/" rel="noopener noreferrer"&gt;maui-dev doctor&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup and page traces&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-performance/" rel="noopener noreferrer"&gt;Plugin.Maui.Performance&lt;/a&gt; / &lt;code&gt;maui-perf&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An exporter the app owns&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-observability/" rel="noopener noreferrer"&gt;Plugin.Maui.Observability&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;adb logcat&lt;/code&gt;, Charles, Firebase Performance, and Sentry remain the tools for raw logs, HTTP, and a report that arrives after the session. Pulse is the table for the plugins you already registered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.Pulse &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.1
dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; Plugin.Maui.Pulse.Cli &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json
adb reverse tcp:7878 tcp:7878
maui-pulse attach &lt;span class="nt"&gt;--package&lt;/span&gt; com.myapp.android &lt;span class="nt"&gt;--android&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 7878
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;UseMauiPulse()&lt;/code&gt;, keep the &lt;code&gt;UseX()&lt;/code&gt; lines you already have, and read a &lt;code&gt;—&lt;/code&gt; as waiting.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.Pulse" rel="noopener noreferrer"&gt;Plugin.Maui.Pulse&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CLI: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.Pulse.Cli" rel="noopener noreferrer"&gt;Plugin.Maui.Pulse.Cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/nuvyntralabs/MauiPulse" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/MauiPulse&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Toolkit page: &lt;a href="https://nuvyntralabs.github.io/toolkits/maui-pulse/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/toolkits/maui-pulse/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>maui</category>
      <category>observability</category>
      <category>apppulse</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>Lock the Device, Not the Page: App Lock, Biometric, Screen Guard, and the Vault</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:04:32 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/lock-the-device-not-the-page-app-lock-biometric-screen-guard-and-the-vault-5bkh</link>
      <guid>https://dev.to/nuvyantralabs_ccee/lock-the-device-not-the-page-app-lock-biometric-screen-guard-and-the-vault-5bkh</guid>
      <description>&lt;p&gt;A banking screen that hides itself with &lt;code&gt;IsVisible&lt;/code&gt; is still in the recents thumbnail. A Face ID call on one button does not lock the app two minutes after the user switches to Messages. A token in &lt;code&gt;Preferences&lt;/code&gt; is not a file in a vault.&lt;/p&gt;

&lt;p&gt;Each of those is a different package. This post wires the five that cover the device: the lock timer, a one-shot prompt, the screenshot flag, small secrets, and encrypted files.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Background, timer, cover, unlock&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.nuget.org/packages/Plugin.Maui.AppLock" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.AppLock&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One Face ID, fingerprint, or device PIN prompt&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.nuget.org/packages/Plugin.Maui.BiometricPlus" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.BiometricPlus&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recents thumbnail and screenshots&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.nuget.org/packages/Plugin.Maui.ScreenGuard" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.ScreenGuard&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A short secret with expiry&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.nuget.org/packages/Plugin.Maui.SecureStoragePlus" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.SecureStoragePlus&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An encrypted file&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.nuget.org/packages/Plugin.Maui.FileVault" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.FileVault&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All five are MIT licensed. AppLock, BiometricPlus, ScreenGuard, and FileVault target Android and iOS. SecureStoragePlus also targets Mac Catalyst and Windows. Access and refresh tokens, 401 refresh, and multi-device revoke stay on &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-secure-session/" rel="noopener noreferrer"&gt;Plugin.Maui.SecureSession&lt;/a&gt;. SecureSession persists through SecureStoragePlus. It is not a second lock timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timer and the cover
&lt;/h2&gt;

&lt;p&gt;AppLock owns the sequence: the app backgrounds, &lt;code&gt;LockAfter&lt;/code&gt; elapses, the app returns, the user authenticates, the app unlocks. Face ID and the device PIN are the unlock step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.AppLock &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Plugin.Maui.AppLock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMauiApp&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAppLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LockAfter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowBiometric&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowDevicePin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoPromptOnResume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TimeSpan.Zero&lt;/code&gt; locks as soon as the app backgrounds. &lt;code&gt;LockOnStart = true&lt;/code&gt; also starts locked after a cold start. &lt;code&gt;UseAppLock&lt;/code&gt; hooks Android pause/resume and iOS background/activate. Without the generic host, call &lt;code&gt;NotifyBackground()&lt;/code&gt; and &lt;code&gt;NotifyForeground()&lt;/code&gt; yourself.&lt;/p&gt;

&lt;p&gt;Swap the window to a cover while the app is locked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;appLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateChanged&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Page&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AppLockState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Locked&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;AppLockState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authenticating&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AppLockPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appLock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mainPage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RequireAuthenticationAsync()&lt;/code&gt; is the gate before the app is usable. A balance screen that must prompt even when the timer has not elapsed uses &lt;code&gt;AppLockPromptMode.Always&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AppLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireAuthenticationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppLockPromptMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Always&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Succeeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;ShowBalance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed or cancelled prompt returns &lt;code&gt;AppLockAuthResult&lt;/code&gt; and leaves the app locked. It does not throw. If the automatic resume prompt throws, &lt;code&gt;AuthenticationCompleted&lt;/code&gt; still fires with a failed result so the cover stays up.&lt;/p&gt;

&lt;p&gt;Android API 23+. &lt;code&gt;USE_BIOMETRIC&lt;/code&gt; is merged from the package. Enroll a fingerprint, face, or device PIN before you test. When &lt;code&gt;AllowDevicePin&lt;/code&gt; is true, the system prompt includes the device credential. iOS needs &lt;code&gt;NSFaceIDUsageDescription&lt;/code&gt; when biometrics are allowed. &lt;code&gt;AllowDevicePin&lt;/code&gt; uses &lt;code&gt;LAPolicy.DeviceOwnerAuthentication&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;samples/Plugin.Maui.AppLock.Sample&lt;/code&gt; is a small vault: enable the lock, change the grace period, lock now, and step up on "View balance".&lt;/p&gt;

&lt;h2&gt;
  
  
  One prompt, without a timer
&lt;/h2&gt;

&lt;p&gt;BiometricPlus answers a single question: is this the enrolled user, right now? The NuGet id is &lt;code&gt;Plugin.Maui.BiometricPlus&lt;/code&gt; because nuget.org already reserved &lt;code&gt;Plugin.Maui.Biometric&lt;/code&gt;. The namespace stays &lt;code&gt;Plugin.Maui.Biometric&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.BiometricPlus &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Plugin.Maui.Biometric&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseBiometric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowDeviceCredential&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultReason&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Unlock"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;availability&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Biometric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAvailabilityAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Biometric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AuthenticateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BiometricRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Unlock payroll"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetAvailabilityAsync&lt;/code&gt; returns &lt;code&gt;Available&lt;/code&gt;, &lt;code&gt;NotEnrolled&lt;/code&gt;, or &lt;code&gt;NotSupported&lt;/code&gt;. Android uses AndroidX &lt;code&gt;BiometricPrompt&lt;/code&gt; (API 23+). Declare &lt;code&gt;USE_BIOMETRIC&lt;/code&gt; and &lt;code&gt;USE_FINGERPRINT&lt;/code&gt; on the host. iOS uses &lt;code&gt;LAContext&lt;/code&gt;. Face ID needs &lt;code&gt;NSFaceIDUsageDescription&lt;/code&gt;. Touch ID and the device passcode do not.&lt;/p&gt;

&lt;p&gt;Use this package for a button. Use AppLock when the whole window has to disappear after background.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recents thumbnail
&lt;/h2&gt;

&lt;p&gt;ScreenGuard is the window flag. It is not the lock, and it is not file encryption.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.ScreenGuard &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Plugin.Maui.ScreenGuard&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseScreenGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProtectOnStart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;ScreenGuard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProtected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balancePage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Protect&lt;/code&gt; and &lt;code&gt;Unprotect&lt;/code&gt; are reference-counted. &lt;code&gt;SetProtected(page, true)&lt;/code&gt; ties a hold to one page. Android sets &lt;code&gt;FLAG_SECURE&lt;/code&gt;: the recents thumbnail is blank, and screenshots are blocked. No extra manifest permission.&lt;/p&gt;

&lt;p&gt;iOS cannot block a screenshot. The plugin raises &lt;code&gt;CaptureStateChanged&lt;/code&gt; and exposes &lt;code&gt;IsCaptured&lt;/code&gt; while the screen is recorded. The host supplies the overlay. Treat an iOS "blocked screenshot" as a bug in the write-up, not a feature of this package.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short secret
&lt;/h2&gt;

&lt;p&gt;SecureStoragePlus sits on MAUI &lt;code&gt;SecureStorage&lt;/code&gt; (Keychain, EncryptedSharedPreferences) and adds an AES-256-GCM envelope. The key name is associated data, so a blob cannot be copied under another name. Values can expire. The ciphertext is what gets stored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.SecureStoragePlus &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSecureStoragePlus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SecureStoragePlus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sessionJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SecureStorageOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExpireIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SecureStoragePlus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// sign in again&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetAsync&lt;/code&gt; on an expired key returns null and removes the value. Strings are stored as-is. Other types are JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SecureStoragePlus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;36&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SecureStoragePlus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After an upgrade, copy named keys out of MAUI &lt;code&gt;SecureStorage&lt;/code&gt; once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SecureStoragePlus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MigrateFromMauiSecureStorageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"oauth_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"refresh_token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;MigrationOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;RemoveSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;OverwriteExisting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;StorageOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SecureStorageOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExpireIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS, add a keychain access group for &lt;code&gt;$(AppIdentifierPrefix)$(CFBundleIdentifier)&lt;/code&gt; so values survive the way you expect. On Android, if Auto Backup restores preferences onto a device that does not have the original key, catch the failed read and call &lt;code&gt;RemoveAllAsync(resetEncryptionKey: true)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A secret that is a file, not a string, belongs in FileVault.&lt;/p&gt;

&lt;h2&gt;
  
  
  The file
&lt;/h2&gt;

&lt;p&gt;FileVault stores app-private files as AES-256-GCM ciphertext. The master key lives in the platform secure store. Each file has its own nonce.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.FileVault &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Plugin.Maui.FileVault&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseFileVault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultTimeToLive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LockOnBackground&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExcludeFromBackup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FileVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteTextAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notes/pin.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;VaultWriteOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TimeToLive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FileVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadTextAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notes/pin.txt"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FileVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStatisticsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Device-key vaults unlock on first use. &lt;code&gt;RequirePassphrase = true&lt;/code&gt; keeps the vault locked until &lt;code&gt;UnlockAsync&lt;/code&gt;. &lt;code&gt;ChangePassphraseAsync&lt;/code&gt; rewraps the key. It does not re-encrypt every file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UseFileVault&lt;/code&gt; purges expired and idle files on resume. On background, when &lt;code&gt;LockOnBackground&lt;/code&gt; is true, the vault locks. The in-memory master key is cleared either way. Reading an expired file deletes it and throws &lt;code&gt;FileVaultException&lt;/code&gt; with &lt;code&gt;FileVaultError.Expired&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RootDirectory&lt;/code&gt; must stay inside the host-chosen folder. &lt;code&gt;VaultName&lt;/code&gt; cannot climb out with &lt;code&gt;..&lt;/code&gt;. &lt;code&gt;DestroyAsync&lt;/code&gt; deletes the files, the manifest, and the stored key. Prefer &lt;code&gt;GetStatisticsAsync&lt;/code&gt;. The synchronous &lt;code&gt;GetStatistics()&lt;/code&gt; gives up after 5 seconds if a write holds the gate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-video-pipeline/" rel="noopener noreferrer"&gt;VideoPipeline&lt;/a&gt; &lt;code&gt;Encrypt(key)&lt;/code&gt; writes its own AES-256-GCM &lt;code&gt;.vault&lt;/code&gt; for one clip. FileVault is the store for documents that outlive that call, with a TTL and a lock when the app backgrounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The app switcher still shows the balance&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LockAfter&lt;/code&gt; is still the default grace, or ScreenGuard is not protecting that window. &lt;code&gt;LockAfter = TimeSpan.Zero&lt;/code&gt; locks immediately. &lt;code&gt;FLAG_SECURE&lt;/code&gt; blanks the Android thumbnail.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Face ID works on a button and the app stays open after background&lt;/td&gt;
&lt;td&gt;That button is BiometricPlus. The timer and the cover are AppLock.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS screenshots still save&lt;/td&gt;
&lt;td&gt;ScreenGuard does not block them. Handle &lt;code&gt;CaptureStateChanged&lt;/code&gt; and show your own overlay.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A session token is still readable after eight hours&lt;/td&gt;
&lt;td&gt;It was written without &lt;code&gt;SecureStorageOptions.ExpireIn&lt;/code&gt;. Expiry is checked on the next read.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A note file opens with no passphrase after background&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LockOnBackground&lt;/code&gt; is false, or this vault is device-key and unlocks on first use. Set &lt;code&gt;RequirePassphrase&lt;/code&gt; when the key must not sit in &lt;code&gt;SecureStorage&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens refresh themselves&lt;/td&gt;
&lt;td&gt;That is SecureSession, composed on SecureStoragePlus. AppLock does not store access tokens.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where the five sit next to each other
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hide the app after it backgrounds, then unlock&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AppLock&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One prompt on one action&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;BiometricPlus&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blank recents, block Android screenshots&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ScreenGuard&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expire a string in the platform secure store&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SecureStoragePlus&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypt a file, with TTL and lock&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FileVault&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login, refresh, logout, biometric unlock of the session&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-secure-session/" rel="noopener noreferrer"&gt;Plugin.Maui.SecureSession&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plugin.Fingerprint and similar biometric libraries are the usual one-shot prompt when the app already uses them. MAUI &lt;code&gt;SecureStorage&lt;/code&gt; is enough when you do not need expiry, listing, or the extra AES-256-GCM envelope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Register AppLock with a two-minute &lt;code&gt;LockAfter&lt;/code&gt;, put &lt;code&gt;AppLockPage&lt;/code&gt; up while &lt;code&gt;StateChanged&lt;/code&gt; says &lt;code&gt;Locked&lt;/code&gt;, and call &lt;code&gt;ScreenGuard.SetProtected&lt;/code&gt; on the balance page. Put the session string in SecureStoragePlus with an expiry. Put the document in FileVault.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AppLock: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.AppLock" rel="noopener noreferrer"&gt;nuget.org&lt;/a&gt; · &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.AppLock" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-app-lock/" rel="noopener noreferrer"&gt;package page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;BiometricPlus: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.BiometricPlus" rel="noopener noreferrer"&gt;nuget.org&lt;/a&gt; · &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.Biometric" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-biometric/" rel="noopener noreferrer"&gt;package page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ScreenGuard: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.ScreenGuard" rel="noopener noreferrer"&gt;nuget.org&lt;/a&gt; · &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.ScreenGuard" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-screen-guard/" rel="noopener noreferrer"&gt;package page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SecureStoragePlus: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.SecureStoragePlus" rel="noopener noreferrer"&gt;nuget.org&lt;/a&gt; · &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.SecureStoragePlus" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-secure-storage-plus/" rel="noopener noreferrer"&gt;package page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FileVault: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.FileVault" rel="noopener noreferrer"&gt;nuget.org&lt;/a&gt; · &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.FileVault" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-file-vault/" rel="noopener noreferrer"&gt;package page&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>maui</category>
      <category>security</category>
      <category>biometric</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>GeoLocator 1.0.8: One Fix, a Tracking Session, and a Reverse Geocode</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:03:58 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/geolocator-108-one-fix-a-tracking-session-and-a-reverse-geocode-4di</link>
      <guid>https://dev.to/nuvyantralabs_ccee/geolocator-108-one-fix-a-tracking-session-and-a-reverse-geocode-4di</guid>
      <description>&lt;p&gt;A field form needs the yard the technician is standing in. A trip screen needs the next fix every few seconds. A label under the pin needs a street, not a pair of numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin.Maui.GeoLocator&lt;/strong&gt; is those three calls. Version &lt;strong&gt;1.0.8&lt;/strong&gt; is on nuget.org. Android and iOS share &lt;code&gt;IGeoLocator&lt;/code&gt;. Mac Catalyst and Windows are not primary targets.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough: a single fix, a tracking session, reverse geocoding, and the permissions the host still declares.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GeoLocator is
&lt;/h2&gt;

&lt;p&gt;GeoLocator is a location plugin for &lt;strong&gt;.NET MAUI&lt;/strong&gt;. MIT licensed. It targets &lt;code&gt;net10.0&lt;/code&gt;, &lt;code&gt;net10.0-android&lt;/code&gt; (API 21+), and &lt;code&gt;net10.0-ios&lt;/code&gt; (15+). On &lt;code&gt;net10.0&lt;/code&gt; the location APIs throw &lt;code&gt;GeoLocatorException&lt;/code&gt; with &lt;code&gt;FeatureNotSupported&lt;/code&gt;, so shared projects can reference the package.&lt;/p&gt;

&lt;p&gt;It does one job: return a position, stream positions until you stop, and turn a coordinate into an address.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Package&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.nuget.org/packages/Plugin.Maui.GeoLocator" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.GeoLocator&lt;/code&gt;&lt;/a&gt; 1.0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseGeoLocator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IGeoLocator&lt;/code&gt;, or &lt;code&gt;GeoLocator.Current&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.GeoLocator" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/Plugin.Maui.GeoLocator&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-geolocator/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/packages/plugin-maui-geolocator/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Enter, exit, and dwell for a circular region stay on &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-geofence/" rel="noopener noreferrer"&gt;Plugin.Maui.Geofence&lt;/a&gt;. GeoLocator does not draw a map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and register
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.GeoLocator &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMauiApp&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseGeoLocator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnableLogging&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resolve &lt;code&gt;IGeoLocator&lt;/code&gt; from dependency injection, or use &lt;code&gt;GeoLocator.Current&lt;/code&gt;. When &lt;code&gt;EnableLogging&lt;/code&gt; is true and the host registered a logger (&lt;code&gt;builder.Logging.AddDebug()&lt;/code&gt;), the plugin writes through &lt;code&gt;ILogger&lt;/code&gt;. You can also call &lt;code&gt;EnableLogging(true)&lt;/code&gt;, pass a &lt;code&gt;DebugGeoLocatorLogger&lt;/code&gt;, or turn it off with &lt;code&gt;EnableLogging(false)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The plugin requests when-in-use location at runtime. Declare the usage strings yourself.&lt;/p&gt;

&lt;p&gt;Android, in &lt;code&gt;Platforms/Android/AndroidManifest.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.ACCESS_COARSE_LOCATION"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.ACCESS_FINE_LOCATION"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS, in &lt;code&gt;Platforms/iOS/Info.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSLocationWhenInUseUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This app needs your location to show where you are.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;GeoLocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentLocationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LocationRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Accuracy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocationAccuracy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last cached fix, when the platform still has one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;GeoLocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetLastKnownLocationAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetCurrentLocationAsync&lt;/code&gt; waits for a new reading. &lt;code&gt;GetLastKnownLocationAsync&lt;/code&gt; returns what the device already stored. A form that must open offline can show the cached fix first and replace it when the new one arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tracking session
&lt;/h2&gt;

&lt;p&gt;Tracking is a session you start and stop. It is built for the foreground.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GeoLocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocationChanged&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocationError&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartTrackingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TrackingOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Accuracy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocationAccuracy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MinimumTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;MinimumDistanceMeters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopTrackingAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MinimumTime&lt;/code&gt; and &lt;code&gt;MinimumDistanceMeters&lt;/code&gt; drop fixes that arrive too soon or too close. Subscribe to &lt;code&gt;LocationChanged&lt;/code&gt; and &lt;code&gt;LocationError&lt;/code&gt; before &lt;code&gt;StartTrackingAsync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Background tracking is a host concern. On Android it needs a foreground service in the app. On iOS it needs &lt;code&gt;AllowBackgroundUpdates = true&lt;/code&gt;, always-authorization text, and the &lt;code&gt;location&lt;/code&gt; background mode. The plugin does not add that service for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse geocoding
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;addresses&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;GeoLocator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReverseGeocodeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;47.6062&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;122.3321&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FormattedAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass the latitude and longitude you already have. The call does not start a tracking session.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GeoLocatorException&lt;/code&gt; / &lt;code&gt;FeatureNotSupported&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The call ran on &lt;code&gt;net10.0&lt;/code&gt; or a target that is not Android or iOS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The prompt never appears&lt;/td&gt;
&lt;td&gt;The manifest or &lt;code&gt;Info.plist&lt;/code&gt; usage string is missing. The plugin requests when-in-use. It does not add the declaration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GetCurrentLocationAsync&lt;/code&gt; times out&lt;/td&gt;
&lt;td&gt;Accuracy is &lt;code&gt;Best&lt;/code&gt; indoors, or location is off on the device. Try the last known fix, then a longer &lt;code&gt;Timeout&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixes arrive after &lt;code&gt;StopTrackingAsync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The handler is still attached. Unsubscribe when the page goes away.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The trace dies when the screen locks&lt;/td&gt;
&lt;td&gt;Foreground tracking stopped with the process. Background needs the host service on Android, or &lt;code&gt;AllowBackgroundUpdates&lt;/code&gt; plus the location background mode on iOS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You wanted "entered the depot"&lt;/td&gt;
&lt;td&gt;That is a region. Use Geofence. GeoLocator answers where the device is.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where GeoLocator sits next to the other tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A current fix, a tracking session, reverse geocoding&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;GeoLocator&lt;/strong&gt; — &lt;code&gt;Plugin.Maui.GeoLocator&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enter, exit, and dwell for up to 20 circles&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-geofence/" rel="noopener noreferrer"&gt;Plugin.Maui.Geofence&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A measured offline trip against a vehicle meter&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/research/gps-sensor-tracking-service/" rel="noopener noreferrer"&gt;GPSSensorTrackingService&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One fix and nothing else&lt;/td&gt;
&lt;td&gt;.NET MAUI &lt;code&gt;Geolocation&lt;/code&gt; already does that&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MAUI &lt;code&gt;Geolocation&lt;/code&gt; and the older Xamarin.Essentials API cover an on-demand fix. GeoLocator is the client when the same type also has to start and stop a session and reverse geocode. A hosted map (Google Maps, Mapbox) is still a separate SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.GeoLocator &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register &lt;code&gt;UseGeoLocator&lt;/code&gt;, declare when-in-use location, and call &lt;code&gt;GetCurrentLocationAsync&lt;/code&gt; once before you start a session. &lt;code&gt;samples/GeoLocator.Sample&lt;/code&gt; exercises the fix, the cache, tracking, and reverse geocoding.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.GeoLocator" rel="noopener noreferrer"&gt;Plugin.Maui.GeoLocator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.GeoLocator" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/Plugin.Maui.GeoLocator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sample: &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.GeoLocator/tree/main/samples/GeoLocator.Sample" rel="noopener noreferrer"&gt;GeoLocator.Sample&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Package page: &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-geolocator/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/packages/plugin-maui-geolocator/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>maui</category>
      <category>gps</category>
      <category>geocoding</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>MauiDev 1.2.2: What maui-dev doctor Checks, and What --fix Will Touch</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:03:24 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/mauidev-122-what-maui-dev-doctor-checks-and-what-fix-will-touch-5cli</link>
      <guid>https://dev.to/nuvyantralabs_ccee/mauidev-122-what-maui-dev-doctor-checks-and-what-fix-will-touch-5cli</guid>
      <description>&lt;p&gt;A MAUI app that fails to build is often a machine problem and a project problem at the same time. The SDK is present. &lt;code&gt;&amp;lt;UseMaui&amp;gt;&lt;/code&gt; is missing. Three splash entries point at the same file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;maui-dev doctor&lt;/strong&gt; reads both. Version &lt;strong&gt;1.2.2&lt;/strong&gt; of &lt;code&gt;Plugin.Maui.MauiDev.Cli&lt;/code&gt; is on nuget.org. It is a global &lt;code&gt;dotnet&lt;/code&gt; tool. It is not a &lt;code&gt;PackageReference&lt;/code&gt; in the app.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough: install the tool, run doctor, and which two edits &lt;code&gt;--fix&lt;/code&gt; is allowed to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doctor is
&lt;/h2&gt;

&lt;p&gt;MauiDev is project-aware. &lt;a href="https://github.com/Redth/dotnet-maui-check" rel="noopener noreferrer"&gt;maui-check&lt;/a&gt; stops at the environment. &lt;code&gt;dotnet workload&lt;/code&gt; and the Microsoft &lt;code&gt;maui&lt;/code&gt; CLI install or list SDKs. Doctor also opens the csproj, the manifests, and the MAUI resource items.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Package&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.nuget.org/packages/Plugin.Maui.MauiDev.Cli" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.MauiDev.Cli&lt;/code&gt;&lt;/a&gt; 1.2.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command&lt;/td&gt;
&lt;td&gt;&lt;code&gt;maui-dev doctor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extension&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://marketplace.visualstudio.com/items?itemName=nuvyntralabs.maui-dev" rel="noopener noreferrer"&gt;&lt;code&gt;nuvyntralabs.maui-dev&lt;/code&gt;&lt;/a&gt; (VS Code / Cursor 1.90+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/nuvyntralabs/MauiDev" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/MauiDev&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/toolkits/maui-dev/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/toolkits/maui-dev/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;nuget.org reserved the id &lt;code&gt;MauiDev.Cli&lt;/code&gt;. The package id is &lt;code&gt;Plugin.Maui.MauiDev.Cli&lt;/code&gt;. The command stays &lt;code&gt;maui-dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; Plugin.Maui.MauiDev.Cli &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json
maui-dev doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not run &lt;code&gt;dotnet add package Plugin.Maui.MauiDev.Cli&lt;/code&gt; in the app. The VS Code / Cursor extension shells out to the same command and offers to install the tool when it is missing.&lt;/p&gt;

&lt;p&gt;On an interactive terminal the CLI asks every 4 hours whether to update from nuget.org (&lt;code&gt;[y/N]&lt;/code&gt;, default no). The answer is cached in &lt;code&gt;~/.nuvyntra/cli-updates.json&lt;/code&gt;. Skip the prompt with &lt;code&gt;--no-update-check&lt;/code&gt;, &lt;code&gt;NUVYNTRA_NO_UPDATE_CHECK=1&lt;/code&gt;, or any &lt;code&gt;--ci&lt;/code&gt; / JSON / SARIF run. &lt;code&gt;--no-update-check&lt;/code&gt; shipped in 1.2.2. The CLI does not phone home.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the report covers
&lt;/h2&gt;

&lt;p&gt;Point &lt;code&gt;--path&lt;/code&gt; at the app when the repo root is not the csproj.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maui-dev doctor &lt;span class="nt"&gt;--path&lt;/span&gt; ./src/App
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The machine section covers the .NET SDK, the MAUI workload, the Android SDK, the JDK, Xcode, and CocoaPods. A check that does not apply on the current OS is skipped.&lt;/p&gt;

&lt;p&gt;The project section covers target frameworks, Android min SDK, permissions, duplicate resources, &lt;code&gt;&amp;lt;UseMaui&amp;gt;&lt;/code&gt;, and signing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET MAUI Developer Doctor
────────────────────────────────────

Machine
────────────────────────────────────

✓ .NET SDK               10.0.102
✓ MAUI workload          maui 10.0.0
✓ Android SDK            ANDROID_HOME set
✓ JDK                    17.0.12
· Xcode                  skipped (Linux)
· CocoaPods              skipped (Linux)

Project
────────────────────────────────────

✗ UseMaui                missing &amp;lt;UseMaui&amp;gt;true&amp;lt;/UseMaui&amp;gt;
✗ Duplicate resources    3 detected
⚠ Android min SDK        21 (recommend 24+)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit &lt;code&gt;0&lt;/code&gt; is pass or skip. Exit &lt;code&gt;1&lt;/code&gt; is a failure, or a warning when &lt;code&gt;--warn-as-error&lt;/code&gt; or &lt;code&gt;--ci&lt;/code&gt; is set. Exit &lt;code&gt;2&lt;/code&gt; is a usage error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two edits --fix may make
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--fix&lt;/code&gt; on doctor does two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Insert &lt;code&gt;&amp;lt;UseMaui&amp;gt;true&amp;lt;/UseMaui&amp;gt;&lt;/code&gt; when the project already looks like MAUI.&lt;/li&gt;
&lt;li&gt;Deduplicate identical &lt;code&gt;MauiSplashScreen&lt;/code&gt;, &lt;code&gt;MauiImage&lt;/code&gt;, &lt;code&gt;MauiIcon&lt;/code&gt;, and &lt;code&gt;MauiFont&lt;/code&gt; items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the dry run first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maui-dev doctor &lt;span class="nt"&gt;--fix&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
maui-dev doctor &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doctor will not bump the min SDK, remove a permission, write a keystore password, install a workload, rewrite a TFM, or push a package. A missing workload is reported. The install command is &lt;code&gt;dotnet workload install maui&lt;/code&gt;, and doctor does not run it. &lt;code&gt;maui-dev workload&lt;/code&gt; prints that command. It does not install either.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--ci&lt;/code&gt; emits JSON and treats warnings as errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;maui-dev doctor --ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failing resource check looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"maui-resources"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Duplicate resources"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3 detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Remove duplicate MauiSplashScreen entries."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canFix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"diagnostics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MD030"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Duplicate MauiSplashScreen 'Resources/Splash/splash.svg'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"App.csproj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--format sarif&lt;/code&gt; feeds the IDE Problems panel. &lt;code&gt;.maui-dev.json&lt;/code&gt; can ignore a diagnostic id such as &lt;code&gt;MD020&lt;/code&gt; or a check id.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maui-dev: command not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The global tool is not on &lt;code&gt;PATH&lt;/code&gt;. Reinstall &lt;code&gt;Plugin.Maui.MauiDev.Cli&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--no-update-check&lt;/code&gt; is an unknown option&lt;/td&gt;
&lt;td&gt;The installed tool is 1.2.1 or older. Update to 1.2.2.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--fix&lt;/code&gt; left the min SDK at 21&lt;/td&gt;
&lt;td&gt;That warning is a recommendation. Doctor does not rewrite min SDK.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate permissions are still there&lt;/td&gt;
&lt;td&gt;Doctor does not dedupe permissions. &lt;code&gt;maui-dev permissions --fix&lt;/code&gt; dedupes identical Android &lt;code&gt;UsesPermission&lt;/code&gt; nodes and never removes one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The app still crashes on device&lt;/td&gt;
&lt;td&gt;Doctor reads the project. Leaks, traces, and crashes stay on the runtime plugins below.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where doctor sits next to the other tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Machine and project checks, with a two-item fix list&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;maui-dev doctor&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event handlers, &lt;code&gt;new HttpClient()&lt;/code&gt;, fire-and-forget&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;maui-dev analyze&lt;/code&gt; (heuristic, not a Roslyn analyzer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store ids and the iOS privacy manifest&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;maui-dev publish --validate&lt;/code&gt; (never pushes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A page still alive after you pop it&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-leak-analyser/" rel="noopener noreferrer"&gt;Plugin.Maui.LeakAnalyser&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live plugin lanes during a Debug session&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/toolkits/maui-pulse/" rel="noopener noreferrer"&gt;Pulse&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; Plugin.Maui.MauiDev.Cli &lt;span class="nt"&gt;--source&lt;/span&gt; https://api.nuget.org/v3/index.json
maui-dev doctor &lt;span class="nt"&gt;--path&lt;/span&gt; ./src/App &lt;span class="nt"&gt;--fix&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the dry run before you let &lt;code&gt;--fix&lt;/code&gt; insert &lt;code&gt;&amp;lt;UseMaui&amp;gt;&lt;/code&gt; or drop a duplicate splash item.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.MauiDev.Cli" rel="noopener noreferrer"&gt;Plugin.Maui.MauiDev.Cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/nuvyntralabs/MauiDev" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/MauiDev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Toolkit page: &lt;a href="https://nuvyntralabs.github.io/toolkits/maui-dev/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/toolkits/maui-dev/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>maui</category>
      <category>cli</category>
      <category>toubleshooting</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>Understanding System Design as a .NET/MAUI Engineer</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:02:18 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/understanding-system-design-as-a-netmaui-engineer-n57</link>
      <guid>https://dev.to/nuvyantralabs_ccee/understanding-system-design-as-a-netmaui-engineer-n57</guid>
      <description>&lt;p&gt;System design is often associated with architects and technical interviews. But every .NET or .NET MAUI developer makes system-design decisions—even when building a small application.&lt;/p&gt;

&lt;p&gt;The difference is &lt;strong&gt;how far you think beyond your code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A developer asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I implement this feature?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A senior developer asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How should this feature interact with the rest of the application?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An architect asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How will the system behave when users, data, traffic, and failures increase?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the journey into system design.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is System Design?
&lt;/h2&gt;

&lt;p&gt;System design defines how different components work together to satisfy both functional and non-functional requirements.&lt;/p&gt;

&lt;p&gt;A simple application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET MAUI
    |
    v
ASP.NET Core API
    |
    v
SQL Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A larger system may look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET MAUI
    |
API Gateway
    |
 ┌──┴──────────┐
 |             |
Services     Services
 |             |
 └──────┬──────┘
        |
   Database / Cache
        |
      Queue
        |
    Background
     Workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part isn't drawing these boxes. It's understanding &lt;strong&gt;why they exist and what happens when they fail&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Design at Different Levels
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Beginner
&lt;/h3&gt;

&lt;p&gt;Focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client/server architecture&lt;/li&gt;
&lt;li&gt;HTTP/HTTPS&lt;/li&gt;
&lt;li&gt;REST APIs&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;Dependency Injection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understand how your MAUI application communicates with the backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intermediate
&lt;/h3&gt;

&lt;p&gt;Start thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Caching&lt;/li&gt;
&lt;li&gt;API scalability&lt;/li&gt;
&lt;li&gt;Background processing&lt;/li&gt;
&lt;li&gt;Retry and timeout&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;API versioning&lt;/li&gt;
&lt;li&gt;Message queues&lt;/li&gt;
&lt;li&gt;Database indexes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Senior
&lt;/h3&gt;

&lt;p&gt;Think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;li&gt;Reliability&lt;/li&gt;
&lt;li&gt;Data consistency&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;li&gt;Offline support&lt;/li&gt;
&lt;li&gt;Observability&lt;/li&gt;
&lt;li&gt;Failure handling&lt;/li&gt;
&lt;li&gt;Cloud architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architect
&lt;/h3&gt;

&lt;p&gt;Think about the complete system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Availability&lt;/li&gt;
&lt;li&gt;Disaster recovery&lt;/li&gt;
&lt;li&gt;Cost&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;li&gt;Compliance&lt;/li&gt;
&lt;li&gt;Infrastructure&lt;/li&gt;
&lt;li&gt;Integration&lt;/li&gt;
&lt;li&gt;Long-term maintainability&lt;/li&gt;
&lt;li&gt;Architectural trade-offs&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  System Design for .NET MAUI
&lt;/h1&gt;

&lt;p&gt;Mobile applications introduce additional challenges.&lt;/p&gt;

&lt;p&gt;A MAUI application may encounter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No network&lt;/li&gt;
&lt;li&gt;Slow connectivity&lt;/li&gt;
&lt;li&gt;Application termination&lt;/li&gt;
&lt;li&gt;Background restrictions&lt;/li&gt;
&lt;li&gt;Token expiration&lt;/li&gt;
&lt;li&gt;Device storage limitations&lt;/li&gt;
&lt;li&gt;Different OS behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A production mobile architecture might therefore look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             .NET MAUI
                 |
       ┌─────────┴─────────┐
       |                   |
   Local SQLite        API Client
       |                   |
       └──── Sync ─────────┘
                 |
              Backend
                 |
        ┌────────┼────────┐
        |        |        |
      API      Cache    Queue
        |                 |
     Database          Workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you're dealing with &lt;strong&gt;mobile system design&lt;/strong&gt;, not just UI architecture.&lt;/p&gt;




&lt;h1&gt;
  
  
  Offline-First Is System Design
&lt;/h1&gt;

&lt;p&gt;Suppose a salesperson creates a customer while offline.&lt;/p&gt;

&lt;p&gt;The application may store the data locally and synchronize it later.&lt;/p&gt;

&lt;p&gt;But what happens if another device modified the same customer?&lt;/p&gt;

&lt;p&gt;You now need a conflict strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Last-write-wins&lt;/li&gt;
&lt;li&gt;Version numbers&lt;/li&gt;
&lt;li&gt;Server-authoritative data&lt;/li&gt;
&lt;li&gt;Conflict detection&lt;/li&gt;
&lt;li&gt;Manual resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are system-design decisions.&lt;/p&gt;




&lt;h1&gt;
  
  
  Reliability Matters
&lt;/h1&gt;

&lt;p&gt;Production systems assume that things &lt;strong&gt;will fail&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A database can become unavailable.&lt;br&gt;
An API can timeout.&lt;br&gt;
A third-party service can fail.&lt;br&gt;
A mobile device can lose connectivity.&lt;/p&gt;

&lt;p&gt;Common techniques include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts&lt;/strong&gt; — don't wait forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries&lt;/strong&gt; — recover from transient failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circuit breakers&lt;/strong&gt; — stop repeatedly calling an unhealthy dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency&lt;/strong&gt; — prevent duplicate operations when requests are retried.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mobile App
    |
    v
API
    |
    v
Queue
    |
    v
Background Worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every operation needs to happen synchronously.&lt;/p&gt;




&lt;h1&gt;
  
  
  Scalability
&lt;/h1&gt;

&lt;p&gt;If your application grows from 1,000 to 100,000 users, architecture becomes important.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vertical scaling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Small Server
     ↓
Bigger Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Horizontal scaling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        Load Balancer
        /     |     \
     API1   API2   API3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Understanding when and why to use these approaches is part of system design.&lt;/p&gt;




&lt;h1&gt;
  
  
  Database Design
&lt;/h1&gt;

&lt;p&gt;Don't simply ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SQL or NoSQL?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What data do we have?&lt;/li&gt;
&lt;li&gt;How are entities related?&lt;/li&gt;
&lt;li&gt;How much data will exist?&lt;/li&gt;
&lt;li&gt;What consistency is required?&lt;/li&gt;
&lt;li&gt;What queries will be frequent?&lt;/li&gt;
&lt;li&gt;How fast must they be?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Indexes&lt;/li&gt;
&lt;li&gt;Transactions&lt;/li&gt;
&lt;li&gt;Replication&lt;/li&gt;
&lt;li&gt;Partitioning&lt;/li&gt;
&lt;li&gt;Query performance&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Microservices Are Not Mandatory
&lt;/h1&gt;

&lt;p&gt;Modern architecture doesn't automatically mean microservices.&lt;/p&gt;

&lt;p&gt;A modular monolith can be perfectly appropriate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ASP.NET Core
├── Identity
├── Customers
├── Orders
├── Payments
└── Notifications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there is a strong reason later, individual modules can become services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture should follow requirements—not trends.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Observability
&lt;/h1&gt;

&lt;p&gt;A production system should tell you what is happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logs
&lt;/h3&gt;

&lt;p&gt;What happened?&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics
&lt;/h3&gt;

&lt;p&gt;How often and how much?&lt;/p&gt;

&lt;h3&gt;
  
  
  Traces
&lt;/h3&gt;

&lt;p&gt;Where did the request spend its time?&lt;/p&gt;

&lt;p&gt;For distributed systems, observability is essential for troubleshooting performance and failures.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Simple System Design Process
&lt;/h1&gt;

&lt;p&gt;When designing a system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Understand requirements
          ↓
2. Estimate scale
          ↓
3. Design high-level architecture
          ↓
4. Design data model
          ↓
5. Identify failure scenarios
          ↓
6. Plan scalability
          ↓
7. Address security
          ↓
8. Add observability
          ↓
9. Evaluate trade-offs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens if this component fails?&lt;/p&gt;

&lt;p&gt;What happens if traffic increases 10×?&lt;/p&gt;

&lt;p&gt;What happens if the request is sent twice?&lt;/p&gt;

&lt;p&gt;What happens when the device is offline?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;System design isn't about memorizing Azure services, drawing complicated diagrams, or building microservices everywhere.&lt;/p&gt;

&lt;p&gt;It is about &lt;strong&gt;thinking beyond your immediate code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Start with the system you're building today and ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does data flow?&lt;/li&gt;
&lt;li&gt;What happens when the network fails?&lt;/li&gt;
&lt;li&gt;What happens when the database is slow?&lt;/li&gt;
&lt;li&gt;How does the system scale?&lt;/li&gt;
&lt;li&gt;How do we handle duplicate requests?&lt;/li&gt;
&lt;li&gt;How do we detect failures?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions gradually move you from &lt;strong&gt;developer thinking to system thinking&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;System design is not about drawing more boxes.&lt;br&gt;
It's about understanding the consequences of the boxes you draw.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>maui</category>
      <category>systemdesign</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>.NET 11, CoreCLR and Google Play’s DEX Optimization: What .NET MAUI Developers Need to Know</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 11:14:30 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/net-11-coreclr-and-google-plays-dex-optimization-what-net-maui-developers-need-to-know-443f</link>
      <guid>https://dev.to/nuvyantralabs_ccee/net-11-coreclr-and-google-plays-dex-optimization-what-net-maui-developers-need-to-know-443f</guid>
      <description>&lt;p&gt;Google Play is introducing more visibility around Android application quality, including &lt;strong&gt;16 KB page-size support&lt;/strong&gt; and &lt;strong&gt;DEX code optimization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For .NET MAUI developers, this raises an interesting question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does moving to .NET 11 and CoreCLR automatically improve the DEX optimization score reported by Google Play?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The short answer is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not directly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;.NET 11 brings important improvements to .NET MAUI Android applications, including CoreCLR, ReadyToRun, and improved trimming. However, Google Play's DEX optimization metric is primarily related to the Android DEX pipeline and should be considered separately from .NET runtime optimization.&lt;/p&gt;

&lt;p&gt;Let's break this down.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is changing in .NET MAUI with .NET 11?
&lt;/h2&gt;

&lt;p&gt;One of the significant changes in .NET 11 is the move toward &lt;strong&gt;CoreCLR as the default runtime for .NET MAUI applications&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Microsoft's official documentation states that CoreCLR is the default runtime for .NET MAUI applications in .NET 11.&lt;/p&gt;

&lt;p&gt;For Android Release builds, CoreCLR also uses &lt;strong&gt;composite partial ReadyToRun&lt;/strong&gt; by default.&lt;/p&gt;

&lt;p&gt;This changes the runtime and compilation model compared with previous MAUI releases.&lt;/p&gt;

&lt;p&gt;The relevant technologies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CoreCLR&lt;/li&gt;
&lt;li&gt;ReadyToRun&lt;/li&gt;
&lt;li&gt;IL trimming&lt;/li&gt;
&lt;li&gt;Android-specific trimming improvements&lt;/li&gt;
&lt;li&gt;NativeAOT as an optional deployment model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These changes can affect startup performance, application size, and runtime behavior.&lt;/p&gt;

&lt;p&gt;However, they should not be confused with Google's DEX optimization metrics.&lt;/p&gt;




&lt;h1&gt;
  
  
  CoreCLR and DEX are different layers
&lt;/h1&gt;

&lt;p&gt;A useful way to understand the architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET MAUI Application
        │
        ├── C# / .NET code
        │       │
        │       ├── CoreCLR
        │       ├── ILLink / Trimming
        │       └── ReadyToRun
        │
        └── Android platform integration
                │
                └── Java/Kotlin / Android code
                        │
                        └── DEX
                              │
                              └── R8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CoreCLR operates primarily on the &lt;strong&gt;.NET runtime side&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;R8 operates on the &lt;strong&gt;Android/DEX side&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET 11 + CoreCLR
        ≠
automatic R8 optimization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upgrading a MAUI application from an older .NET version to .NET 11 does not automatically mean that Google Play will report a high DEX optimization percentage.&lt;/p&gt;




&lt;h1&gt;
  
  
  What exactly is Google Play measuring?
&lt;/h1&gt;

&lt;p&gt;Google has introduced a DEX code optimization metric in Play Console.&lt;/p&gt;

&lt;p&gt;According to Google's Android documentation, the metric applies to applications with at least &lt;strong&gt;10 MB of uncompressed DEX code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Google evaluates aspects including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DEX optimization&lt;/li&gt;
&lt;li&gt;Code shrinking&lt;/li&gt;
&lt;li&gt;Code obfuscation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google currently specifies a &lt;strong&gt;25% minimum threshold&lt;/strong&gt; for the relevant optimization metrics.&lt;/p&gt;

&lt;p&gt;This is separate from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target SDK&lt;/li&gt;
&lt;li&gt;64-bit support&lt;/li&gt;
&lt;li&gt;16 KB page-size support&lt;/li&gt;
&lt;li&gt;.NET trimming&lt;/li&gt;
&lt;li&gt;CoreCLR&lt;/li&gt;
&lt;li&gt;ReadyToRun&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are related to Android application quality, but they are not the same measurement.&lt;/p&gt;




&lt;h1&gt;
  
  
  What about 16 KB page-size support?
&lt;/h1&gt;

&lt;p&gt;This is another area that can easily become confused with DEX optimization.&lt;/p&gt;

&lt;p&gt;Android's 16 KB page-size requirement primarily concerns &lt;strong&gt;native libraries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib*.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native libraries must be compatible with devices using 16 KB memory pages.&lt;/p&gt;

&lt;p&gt;DEX optimization, on the other hand, concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;classes.dex
classes2.dex
classes3.dex
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So an application can simultaneously have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16 KB page-size support       ✅
Target SDK 36                 ✅
64-bit architectures          ✅
DEX optimization              ⚠️ Low
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no contradiction.&lt;/p&gt;

&lt;p&gt;They represent different parts of the Android build.&lt;/p&gt;




&lt;h1&gt;
  
  
  Does .NET 11 improve application size?
&lt;/h1&gt;

&lt;p&gt;Potentially, yes.&lt;/p&gt;

&lt;p&gt;This is where .NET 11 becomes interesting.&lt;/p&gt;

&lt;p&gt;Microsoft has made improvements to Android trimming in .NET 11.&lt;/p&gt;

&lt;p&gt;The Android trimmer can remove unused code more effectively, including improvements around Android-related managed types.&lt;/p&gt;

&lt;p&gt;This can reduce the amount of code included in an application.&lt;/p&gt;

&lt;p&gt;However, application size and DEX optimization are not interchangeable measurements.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET trimming
      ↓
Remove unused .NET code
      ↓
Smaller managed application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;R8
      ↓
Analyze Android bytecode
      ↓
Shrink / optimize / obfuscate DEX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are valuable, but they operate at different stages.&lt;/p&gt;




&lt;h1&gt;
  
  
  ReadyToRun is another separate concept
&lt;/h1&gt;

&lt;p&gt;.NET 11 Android Release builds use composite partial ReadyToRun with CoreCLR by default.&lt;/p&gt;

&lt;p&gt;ReadyToRun precompiles portions of .NET code to improve startup performance.&lt;/p&gt;

&lt;p&gt;But ReadyToRun should not be interpreted as a DEX optimization mechanism.&lt;/p&gt;

&lt;p&gt;In fact, precompiled code can introduce a size trade-off because additional native code may be included.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ReadyToRun is primarily a runtime/startup optimization, not a Google Play DEX optimization mechanism.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This distinction becomes important when analyzing application size and Play Console metrics.&lt;/p&gt;




&lt;h1&gt;
  
  
  What should MAUI developers do about "DEX optimization: Low"?
&lt;/h1&gt;

&lt;p&gt;Don't immediately change your entire MAUI build configuration.&lt;/p&gt;

&lt;p&gt;First, determine what Google Play is actually measuring.&lt;/p&gt;

&lt;p&gt;If your Play Console reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DEX Code Optimization: Low
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;start by inspecting the actual Android App Bundle.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip &lt;span class="nt"&gt;-l&lt;/span&gt; MyApp.aab | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'\.dex$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also calculate the total DEX size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip &lt;span class="nt"&gt;-l&lt;/span&gt; MyApp.aab &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'\.dex$'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>dotnet</category>
      <category>mobile</category>
      <category>performance</category>
    </item>
    <item>
      <title>Migrating a Large Enterprise Xamarin Application to .NET MAUI: Practical Strategies That Actually Work</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:05:16 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/migrating-a-large-xamarin-application-to-net-maui-practical-strategies-that-actually-work-3lpa</link>
      <guid>https://dev.to/nuvyantralabs_ccee/migrating-a-large-xamarin-application-to-net-maui-practical-strategies-that-actually-work-3lpa</guid>
      <description>&lt;p&gt;Migrating a small Xamarin.Forms application to .NET MAUI is usually manageable.&lt;/p&gt;

&lt;p&gt;Migrating a large enterprise application is a different story.&lt;/p&gt;

&lt;p&gt;The difficulty is rarely the &lt;code&gt;Xamarin.Forms&lt;/code&gt; namespace replacement or converting a &lt;code&gt;.csproj&lt;/code&gt; file. The real challenge is that an enterprise application has accumulated years of assumptions around Xamarin, third-party libraries, native platform APIs, custom renderers, background processing, authentication, local databases, CI/CD, and production behavior.&lt;/p&gt;

&lt;p&gt;Microsoft provides migration tooling and official migration paths, but the tooling should be treated as an accelerator—not as the migration strategy itself. The Upgrade Assistant can perform many project and namespace transformations, but additional manual work is expected.&lt;/p&gt;

&lt;p&gt;The practical question therefore becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do you migrate a large Xamarin application while continuing to develop, test, and release the product?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article focuses on that problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Don't Start With the &lt;code&gt;.csproj&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One of the most common mistakes is starting the migration by changing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then fixing whatever breaks.&lt;/p&gt;

&lt;p&gt;That approach works reasonably well for smaller applications.&lt;/p&gt;

&lt;p&gt;For a large application, it creates a huge error list before you understand where the real risks are.&lt;/p&gt;

&lt;p&gt;Instead, start by answering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many screens exist?&lt;/li&gt;
&lt;li&gt;Which screens are business-critical?&lt;/li&gt;
&lt;li&gt;Which NuGet packages are Xamarin-specific?&lt;/li&gt;
&lt;li&gt;Which controls use custom renderers?&lt;/li&gt;
&lt;li&gt;Which features depend on native Android/iOS code?&lt;/li&gt;
&lt;li&gt;Are there background services?&lt;/li&gt;
&lt;li&gt;How are push notifications implemented?&lt;/li&gt;
&lt;li&gt;How does offline synchronization work?&lt;/li&gt;
&lt;li&gt;Which parts depend on third-party SDKs?&lt;/li&gt;
&lt;li&gt;How is the application signed and distributed?&lt;/li&gt;
&lt;li&gt;Which production workflows cannot fail?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first deliverable should be a &lt;strong&gt;dependency and feature map&lt;/strong&gt;, not a converted project.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Divide the Application Into Migration Units
&lt;/h1&gt;

&lt;p&gt;A 200-screen application should not be treated as one migration task.&lt;/p&gt;

&lt;p&gt;Break it into units.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
│
├── Authentication
├── Customer
│   ├── Customer Search
│   ├── Customer Details
│   └── Customer History
│
├── Orders
│   ├── Order Creation
│   ├── Order Editing
│   └── Order Submission
│
├── Offline
│   ├── Local Database
│   ├── Queue
│   └── Synchronization
│
├── Notifications
├── Location
├── Documents
└── Reporting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each unit should have its own migration status.&lt;/p&gt;

&lt;p&gt;A simple tracking model is enough:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Xamarin&lt;/th&gt;
&lt;th&gt;MAUI&lt;/th&gt;
&lt;th&gt;Dependency Risk&lt;/th&gt;
&lt;th&gt;Testing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Migrated&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Automated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer Search&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Migrated&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Regression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order Creation&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;In progress&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Full workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push Notifications&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;In progress&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Device testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline Sync&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Not started&lt;/td&gt;
&lt;td&gt;Very High&lt;/td&gt;
&lt;td&gt;Scenario testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This changes the migration from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The application is 60% migrated."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Authentication and customer workflows are migrated; offline synchronization and notifications remain."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is much more useful to an engineering team.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Freeze Architecture, Not Development
&lt;/h1&gt;

&lt;p&gt;A large enterprise application may need to continue receiving new features while migration is happening.&lt;/p&gt;

&lt;p&gt;This creates a difficult situation.&lt;/p&gt;

&lt;p&gt;The Xamarin application is still in production, but the team is building the future MAUI version.&lt;/p&gt;

&lt;p&gt;Do not freeze all development for months.&lt;/p&gt;

&lt;p&gt;Instead, establish a rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New feature
     │
     ├── Business logic → Shared modern layer
     │
     ├── Xamarin UI → Temporary implementation
     │
     └── MAUI UI → Preferred implementation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that &lt;strong&gt;new business logic should not become more dependent on Xamarin&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, avoid introducing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Xamarin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Forms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service can then be consumed by both generations of the application.&lt;/p&gt;

&lt;p&gt;This is one of the most effective ways to prevent the migration from becoming a moving target.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Use a Compatibility Layer Instead of Rewriting Everything
&lt;/h1&gt;

&lt;p&gt;Large Xamarin applications often contain hundreds of dependencies on platform-specific functionality.&lt;/p&gt;

&lt;p&gt;Trying to replace everything immediately creates unnecessary work.&lt;/p&gt;

&lt;p&gt;Introduce interfaces around unstable or platform-specific functionality.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDeviceService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetDeviceIdAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;IsNetworkAvailableAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation can change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin
   ↓
XamarinDeviceService

MAUI
   ↓
MauiDeviceService
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application code does not need to know which implementation is being used.&lt;/p&gt;

&lt;p&gt;This approach is particularly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device information&lt;/li&gt;
&lt;li&gt;Secure storage&lt;/li&gt;
&lt;li&gt;File access&lt;/li&gt;
&lt;li&gt;Connectivity&lt;/li&gt;
&lt;li&gt;Location&lt;/li&gt;
&lt;li&gt;Notifications&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Background processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to create abstractions everywhere.&lt;/p&gt;

&lt;p&gt;Create them specifically where &lt;strong&gt;migration or platform differences are expected&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Treat Custom Renderers as a Separate Migration Problem
&lt;/h1&gt;

&lt;p&gt;Custom renderers are frequently one of the biggest sources of migration effort.&lt;/p&gt;

&lt;p&gt;A typical Xamarin application may contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CustomRenderer
├── Android
├── iOS
├── Shared Control
└── Platform-specific behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In MAUI, handlers provide the preferred customization model.&lt;/p&gt;

&lt;p&gt;However, you do not necessarily need to rewrite every renderer on day one. MAUI can reuse certain Xamarin.Forms renderer implementations through compatibility/shim support, although this approach has limitations and becomes more important to revisit as you move to newer MAUI versions.&lt;/p&gt;

&lt;p&gt;A practical strategy is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Existing Renderer
       │
       ├── Simple + stable
       │       ↓
       │   Temporary reuse
       │
       ├── Complex but required
       │       ↓
       │   Migrate later
       │
       └── Obsolete behavior
               ↓
           Delete it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not automatically convert every renderer to a handler.&lt;/p&gt;

&lt;p&gt;First ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why does this renderer exist?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes the requirement disappeared years ago.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Be Aggressive About Removing Dead Xamarin Code
&lt;/h1&gt;

&lt;p&gt;Migration is an excellent opportunity to identify code that no longer has a purpose.&lt;/p&gt;

&lt;p&gt;Large Xamarin applications often contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old platform workarounds&lt;/li&gt;
&lt;li&gt;Deprecated controls&lt;/li&gt;
&lt;li&gt;Duplicate services&lt;/li&gt;
&lt;li&gt;Unused renderers&lt;/li&gt;
&lt;li&gt;Legacy navigation code&lt;/li&gt;
&lt;li&gt;Old dependency injection registrations&lt;/li&gt;
&lt;li&gt;Abandoned feature flags&lt;/li&gt;
&lt;li&gt;Xamarin-specific utilities&lt;/li&gt;
&lt;li&gt;Compatibility packages added years ago&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't migrate these blindly.&lt;/p&gt;

&lt;p&gt;Use this decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does the code still provide business value?
        │
        ├── Yes → Migrate
        │
        ├── Maybe → Validate usage
        │
        └── No → Delete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line of legacy code that you remove is one less thing the MAUI application needs to maintain.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Migrate Dependencies Before UI
&lt;/h1&gt;

&lt;p&gt;A common approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Page 1
Page 2
Page 3
Page 4
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks productive, but it can be misleading.&lt;/p&gt;

&lt;p&gt;Suppose 80 pages depend on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OldControls
OldDatabase
OldNavigation
OldAuthentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrating the pages first means the same dependency problems appear repeatedly.&lt;/p&gt;

&lt;p&gt;Instead, migrate foundational dependencies first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Client
     ↓
Authentication
     ↓
Storage
     ↓
Database
     ↓
Telemetry
     ↓
Navigation
     ↓
UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the foundation is stable, migrating individual screens becomes significantly more predictable.&lt;/p&gt;

&lt;p&gt;Microsoft's migration guidance similarly recommends updating dependencies and ensuring the Xamarin.Forms application is stable before moving further into the migration.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. Don't Assume Every NuGet Package Has a MAUI Replacement
&lt;/h1&gt;

&lt;p&gt;This is where many enterprise migrations get stuck.&lt;/p&gt;

&lt;p&gt;You may discover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin Package
       ↓
No MAUI version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't immediately search for a package with a similar name.&lt;/p&gt;

&lt;p&gt;Classify the dependency:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1 — Replace
&lt;/h3&gt;

&lt;p&gt;There is a maintained MAUI/.NET equivalent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2 — Remove
&lt;/h3&gt;

&lt;p&gt;The application no longer needs it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3 — Wrap
&lt;/h3&gt;

&lt;p&gt;The package can remain behind an interface temporarily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 4 — Fork
&lt;/h3&gt;

&lt;p&gt;The package is important but abandoned and the organization controls the source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 5 — Rewrite
&lt;/h3&gt;

&lt;p&gt;The dependency is small enough to replace with internal code.&lt;/p&gt;

&lt;p&gt;The worst strategy is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's keep searching until we find something that looks similar."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A package replacement can introduce completely different runtime behavior.&lt;/p&gt;




&lt;h1&gt;
  
  
  9. Separate UI Migration From Business Logic Migration
&lt;/h1&gt;

&lt;p&gt;Suppose you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderPage
OrderViewModel
OrderService
OrderRepository
OrderDatabase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not migrate these as one block.&lt;/p&gt;

&lt;p&gt;A better approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderService       → Modern .NET
OrderRepository    → Modern .NET
OrderDatabase      → Modern .NET
OrderViewModel     → Modernized
OrderPage          → MAUI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the business layer to stabilize before the UI is converted.&lt;/p&gt;

&lt;p&gt;The result should look approximately like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             ┌───────────────┐
             │   MAUI UI     │
             └───────┬───────┘
                     │
             ┌───────▼───────┐
             │  ViewModel    │
             └───────┬───────┘
                     │
             ┌───────▼───────┐
             │ Application   │
             │   Services    │
             └───────┬───────┘
                     │
             ┌───────▼───────┐
             │ Infrastructure│
             └───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more valuable than simply changing namespaces.&lt;/p&gt;




&lt;h1&gt;
  
  
  10. Build One Complete Vertical Slice
&lt;/h1&gt;

&lt;p&gt;Don't try to migrate 30 screens simultaneously.&lt;/p&gt;

&lt;p&gt;Pick one representative business workflow.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login
  ↓
Customer Search
  ↓
Customer Details
  ↓
Create Order
  ↓
Save Locally
  ↓
Submit
  ↓
Receive Notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrate the complete workflow.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because this exposes problems that a single migrated screen will not reveal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigation&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;API calls&lt;/li&gt;
&lt;li&gt;Dependency injection&lt;/li&gt;
&lt;li&gt;Local storage&lt;/li&gt;
&lt;li&gt;Serialization&lt;/li&gt;
&lt;li&gt;Native APIs&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Push notifications&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;App lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the vertical slice works, you have a template for subsequent migrations.&lt;/p&gt;




&lt;h1&gt;
  
  
  11. Keep Xamarin and MAUI Builds Running in Parallel
&lt;/h1&gt;

&lt;p&gt;For large applications, one of the safest approaches is to maintain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Shared Core
                 │
        ┌────────┴────────┐
        │                 │
   Xamarin App        MAUI App
        │                 │
     Production        Migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared layer contains business logic and infrastructure that can be reused.&lt;/p&gt;

&lt;p&gt;The UI/platform layer gradually moves to MAUI.&lt;/p&gt;

&lt;p&gt;This prevents the migration branch from becoming isolated for six months.&lt;/p&gt;

&lt;p&gt;It also means developers can continue delivering fixes to the existing application.&lt;/p&gt;




&lt;h1&gt;
  
  
  12. Make the CI Pipeline a Migration Gate
&lt;/h1&gt;

&lt;p&gt;A migration is incomplete if it only works on a developer's machine.&lt;/p&gt;

&lt;p&gt;The CI pipeline should build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android
iOS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and eventually all supported targets.&lt;/p&gt;

&lt;p&gt;At minimum, validate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Restore
   ↓
Build
   ↓
Unit Tests
   ↓
UI/Integration Tests
   ↓
Signing
   ↓
Package
   ↓
Artifact Validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also validate the things developers frequently forget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App identifier&lt;/li&gt;
&lt;li&gt;Version number&lt;/li&gt;
&lt;li&gt;Signing certificates&lt;/li&gt;
&lt;li&gt;Provisioning profiles&lt;/li&gt;
&lt;li&gt;Android keystore&lt;/li&gt;
&lt;li&gt;Entitlements&lt;/li&gt;
&lt;li&gt;Push notification configuration&lt;/li&gt;
&lt;li&gt;Environment configuration&lt;/li&gt;
&lt;li&gt;API endpoints&lt;/li&gt;
&lt;li&gt;Release symbols&lt;/li&gt;
&lt;li&gt;Crash reporting&lt;/li&gt;
&lt;li&gt;Store packaging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A successful local build is not equivalent to a successful enterprise migration.&lt;/p&gt;




&lt;h1&gt;
  
  
  13. Test Native Features on Real Devices Early
&lt;/h1&gt;

&lt;p&gt;One of the biggest mistakes is leaving native functionality until the end.&lt;/p&gt;

&lt;p&gt;The application may compile perfectly while these features fail at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push notifications
Background services
Location
Bluetooth
Camera
File picker
Deep links
Biometric authentication
Audio
Native SDKs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;strong&gt;native-risk prototype&lt;/strong&gt; early.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAUI Prototype
   ├── Push notification
   ├── Background task
   ├── Location
   ├── Camera
   └── Third-party SDK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on actual Android and iOS devices.&lt;/p&gt;

&lt;p&gt;This gives you answers before hundreds of screens depend on those capabilities.&lt;/p&gt;




&lt;h1&gt;
  
  
  14. Pay Special Attention to Android and iOS Lifecycle Differences
&lt;/h1&gt;

&lt;p&gt;Xamarin applications often contain lifecycle assumptions that are no longer safe to carry forward unchanged.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application startup
Activity lifecycle
Background execution
Notification handling
Permission flow
Process termination
App resume
App sleep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't test only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Does the screen open?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open app
↓
Background app
↓
Kill app
↓
Receive notification
↓
Tap notification
↓
Resume app
↓
Network unavailable
↓
Network restored
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enterprise bugs frequently appear in these transitions rather than in normal UI navigation.&lt;/p&gt;




&lt;h1&gt;
  
  
  15. Offline Synchronization Deserves Its Own Migration Plan
&lt;/h1&gt;

&lt;p&gt;Offline applications are particularly difficult to migrate.&lt;/p&gt;

&lt;p&gt;Do not treat offline storage as simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Move SQLite code to MAUI."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You need to validate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create offline record
        ↓
Modify offline record
        ↓
Queue operation
        ↓
Application terminated
        ↓
Network restored
        ↓
Synchronize
        ↓
Server conflict
        ↓
Retry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare Xamarin and MAUI behavior using the same test dataset.&lt;/p&gt;

&lt;p&gt;For synchronization systems, correctness is much more important than simply getting the database to compile.&lt;/p&gt;




&lt;h1&gt;
  
  
  16. Use Feature Flags During the Transition
&lt;/h1&gt;

&lt;p&gt;Feature flags can reduce migration risk.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FeatureFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMauiOrderFlow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_mauiOrderFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_legacyOrderFlow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows a migration to be released gradually.&lt;/p&gt;

&lt;p&gt;Possible rollout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internal users
      ↓
QA
      ↓
10% users
      ↓
25% users
      ↓
50% users
      ↓
100% users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact rollout depends on the organization's release process, but the principle is important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't make the first production release the first real-world test.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  17. Compare Production Metrics, Not Just Screens
&lt;/h1&gt;

&lt;p&gt;A MAUI application can look identical to the Xamarin version and still behave differently.&lt;/p&gt;

&lt;p&gt;Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup time
Memory usage
Crash rate
ANR rate
API failures
Login success
Screen load time
Database operations
Push delivery
Background task success
Synchronization failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Metric                  Xamarin       MAUI

Startup                 2.8 sec       3.1 sec
Crash-free sessions     99.4%         99.5%
Sync failures           0.8%          0.7%
Login failures          0.4%          0.5%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact numbers aren't important here.&lt;/p&gt;

&lt;p&gt;The important thing is having measurable evidence instead of relying on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It seems to work."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  18. Create a Migration Definition of Done
&lt;/h1&gt;

&lt;p&gt;A screen should not be considered migrated merely because it compiles.&lt;/p&gt;

&lt;p&gt;For each feature, define:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] UI migrated
[ ] Navigation verified
[ ] API verified
[ ] Authentication verified
[ ] Offline behavior verified
[ ] Error handling verified
[ ] Analytics verified
[ ] Logging verified
[ ] Android tested
[ ] iOS tested
[ ] Regression tests passed
[ ] Production metrics available
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For native features, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] App backgrounded
[ ] App terminated
[ ] App restarted
[ ] Permission denied
[ ] Permission granted
[ ] Network unavailable
[ ] Network restored
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns "migration complete" into something measurable.&lt;/p&gt;




&lt;h1&gt;
  
  
  19. What I Would Avoid
&lt;/h1&gt;

&lt;p&gt;There are several approaches that look attractive but create problems later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Big-bang migration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stop Xamarin development
        ↓
Migrate everything
        ↓
Test everything
        ↓
Release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a very large feedback loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic conversion followed by mass bug fixing
&lt;/h3&gt;

&lt;p&gt;Automation is useful, but it cannot understand business behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rewriting the entire application
&lt;/h3&gt;

&lt;p&gt;Migration is not automatically a reason to rewrite business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating every renderer immediately
&lt;/h3&gt;

&lt;p&gt;Some renderers can be temporarily reused or may no longer be required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replacing every NuGet package
&lt;/h3&gt;

&lt;p&gt;A replacement package is not necessarily behaviorally equivalent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing only the happy path
&lt;/h3&gt;

&lt;p&gt;Enterprise applications fail in lifecycle, offline, permission, synchronization, and recovery scenarios.&lt;/p&gt;




&lt;h1&gt;
  
  
  20. A More Practical Migration Model
&lt;/h1&gt;

&lt;p&gt;For a large enterprise application, I would structure the work like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             EXISTING XAMARIN
                    │
                    ▼
          ┌──────────────────┐
          │ Dependency Audit │
          └────────┬─────────┘
                   │
                   ▼
          ┌──────────────────┐
          │ Modern Core      │
          │ Services         │
          └────────┬─────────┘
                   │
          ┌────────┴────────┐
          ▼                 ▼
    Xamarin UI          MAUI UI
          │                 │
          │          Vertical Slice
          │                 │
          └────────┬────────┘
                   ▼
             Feature Flags
                   │
                   ▼
              Canary Users
                   │
                   ▼
            Production MAUI
                   │
                   ▼
          Remove Xamarin Code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is essentially a &lt;strong&gt;controlled strangler approach&lt;/strong&gt; rather than a rewrite.&lt;/p&gt;

&lt;p&gt;The old application continues to work while the new implementation progressively takes over.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;The hardest part of Xamarin-to-MAUI migration isn't learning .NET MAUI.&lt;/p&gt;

&lt;p&gt;Most experienced Xamarin developers can learn the MAUI differences relatively quickly.&lt;/p&gt;

&lt;p&gt;The difficult part is managing the &lt;strong&gt;technical dependencies, production risk, and migration sequencing&lt;/strong&gt; of a large application.&lt;/p&gt;

&lt;p&gt;The most effective practical principles are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Map the application before changing it.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrate dependencies before hundreds of screens.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep business logic independent of Xamarin.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use compatibility layers where they reduce risk.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don't migrate obsolete code.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build one complete vertical slice first.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep Xamarin and MAUI development running in parallel where necessary.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test native functionality on real devices early.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use feature flags and controlled rollout.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure production behavior instead of judging migration by compilation.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;.NET MAUI gives organizations a modern platform, but the migration itself should be treated as an engineering transformation rather than a project-file conversion.&lt;/p&gt;

&lt;p&gt;The goal isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"We converted Xamarin to MAUI."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real goal is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"We moved the production application to MAUI without losing business functionality, reliability, or the ability to keep delivering."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction is what makes a large migration manageable.&lt;/p&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>xamarin</category>
      <category>maui</category>
      <category>migration</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>From Xamarin to .NET MAUI: An Enterprise Migration Playbook</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Thu, 24 Sep 2026 14:50:19 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/from-xamarin-to-net-maui-an-enterprise-migration-playbook-5g23</link>
      <guid>https://dev.to/nuvyantralabs_ccee/from-xamarin-to-net-maui-an-enterprise-migration-playbook-5g23</guid>
      <description>&lt;p&gt;Migrating a small Xamarin.Forms application to .NET MAUI can be relatively straightforward.&lt;/p&gt;

&lt;p&gt;Migrating a &lt;strong&gt;large enterprise mobile application&lt;/strong&gt; is a completely different problem.&lt;/p&gt;

&lt;p&gt;Enterprise applications often contain years of accumulated business logic, hundreds of screens, custom renderers, native Android and iOS integrations, third-party SDKs, background services, push notifications, offline synchronization, authentication systems, CI/CD pipelines, and production dependencies.&lt;/p&gt;

&lt;p&gt;Simply converting the &lt;code&gt;.csproj&lt;/code&gt; file and fixing compilation errors is not an enterprise migration strategy.&lt;/p&gt;

&lt;p&gt;A successful migration should be treated as a &lt;strong&gt;controlled transformation program&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The objective isn't to convert Xamarin code to MAUI. The objective is to move a production application to a modern platform without disrupting the business.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This guide presents a practical approach for migrating large Xamarin/Xamarin.Forms enterprise applications to .NET MAUI.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why Enterprise Migration Is Different
&lt;/h2&gt;

&lt;p&gt;A typical Xamarin application may look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mobile Application
 ├── Pages
 ├── ViewModels
 ├── Services
 ├── API
 └── Local Storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A large enterprise application can look more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enterprise Mobile Platform
│
├── Presentation
│   ├── 100+ Pages
│   ├── Custom Controls
│   ├── Renderers
│   └── Behaviors
│
├── Business Layer
│   ├── Domain Logic
│   ├── Workflows
│   └── Validation
│
├── Infrastructure
│   ├── REST APIs
│   ├── Database
│   ├── Caching
│   └── Synchronization
│
├── Platform Services
│   ├── Push Notifications
│   ├── Location
│   ├── Bluetooth
│   ├── Background Services
│   └── File System
│
├── Native Integrations
│   ├── Android SDKs
│   ├── iOS SDKs
│   └── Third-party SDKs
│
└── DevOps
    ├── CI/CD
    ├── Signing
    ├── Distribution
    └── Monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of these areas can introduce migration risk.&lt;/p&gt;

&lt;p&gt;Therefore, the migration needs to be planned before the first code change.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. The Migration Mindset
&lt;/h1&gt;

&lt;p&gt;The traditional approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin
   ↓
Upgrade Project
   ↓
Fix Compilation Errors
   ↓
Fix Runtime Errors
   ↓
Release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an enterprise application, a better approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ASSESS
   ↓
BASELINE
   ↓
DECOUPLE
   ↓
BUILD MAUI FOUNDATION
   ↓
PILOT
   ↓
MIGRATE IN WAVES
   ↓
VALIDATE
   ↓
CANARY RELEASE
   ↓
FULL ROLLOUT
   ↓
RETIRE XAMARIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates checkpoints throughout the migration rather than discovering major problems near the end.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Phase 1 — Assess the Existing Application
&lt;/h1&gt;

&lt;p&gt;Before migrating anything, create a &lt;strong&gt;Migration Inventory&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Xamarin.Forms version&lt;/li&gt;
&lt;li&gt;.NET/Xamarin versions&lt;/li&gt;
&lt;li&gt;Target Android/iOS versions&lt;/li&gt;
&lt;li&gt;NuGet dependencies&lt;/li&gt;
&lt;li&gt;Custom renderers&lt;/li&gt;
&lt;li&gt;Effects&lt;/li&gt;
&lt;li&gt;Behaviors&lt;/li&gt;
&lt;li&gt;Custom controls&lt;/li&gt;
&lt;li&gt;Native Android code&lt;/li&gt;
&lt;li&gt;Native iOS code&lt;/li&gt;
&lt;li&gt;Background services&lt;/li&gt;
&lt;li&gt;Push notification implementation&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Local database&lt;/li&gt;
&lt;li&gt;Offline synchronization&lt;/li&gt;
&lt;li&gt;File handling&lt;/li&gt;
&lt;li&gt;Deep links&lt;/li&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;li&gt;Crash reporting&lt;/li&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;li&gt;Signing and provisioning&lt;/li&gt;
&lt;li&gt;Automated tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What parts of this application are actually dependent on Xamarin?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  4. Build a Dependency Risk Matrix
&lt;/h1&gt;

&lt;p&gt;Not every dependency deserves the same migration strategy.&lt;/p&gt;

&lt;p&gt;Classify dependencies into three categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Green
&lt;/h3&gt;

&lt;p&gt;Already compatible with the target .NET/MAUI environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET libraries
Modern NuGet packages
Pure business logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Yellow
&lt;/h3&gt;

&lt;p&gt;Requires configuration or code changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Older NuGet packages
Custom controls
Navigation libraries
Storage libraries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Red
&lt;/h3&gt;

&lt;p&gt;Requires replacement or significant redesign.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin-only packages
Unsupported renderers
Old native bindings
Unmaintained SDKs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the team a migration backlog before implementation begins.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Phase 2 — Establish a Production Baseline
&lt;/h1&gt;

&lt;p&gt;One of the most overlooked parts of migration is establishing measurable baseline data.&lt;/p&gt;

&lt;p&gt;Before migration, measure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Xamarin Baseline&lt;/th&gt;
&lt;th&gt;MAUI Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold startup&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm startup&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory usage&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application size&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login time&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen load time&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API failure rate&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash rate&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ANR rate&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database performance&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Also document critical business workflows.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login
Search Customer
Open Customer
Create Order
Submit Order
Offline
Synchronize
Receive Notification
Logout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These become migration acceptance criteria.&lt;/p&gt;

&lt;p&gt;Without a baseline, "migration completed" can mean nothing more than "the application builds."&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Phase 3 — Stabilize Xamarin Before Migration
&lt;/h1&gt;

&lt;p&gt;If the existing application is on an older Xamarin.Forms version, stabilize it before starting the MAUI migration.&lt;/p&gt;

&lt;p&gt;A useful sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Legacy Xamarin.Forms
        ↓
Latest stable Xamarin.Forms version
        ↓
Update dependencies
        ↓
Fix existing issues
        ↓
Production baseline
        ↓
MAUI migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not combine an unstable legacy application with a platform migration.&lt;/p&gt;

&lt;p&gt;Otherwise, you won't know whether a problem was introduced by the migration or already existed.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Don't Automatically Convert Everything
&lt;/h1&gt;

&lt;p&gt;A common mistake is assuming that every piece of existing code should survive.&lt;/p&gt;

&lt;p&gt;Ask three questions:&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this code contain business logic?
&lt;/h3&gt;

&lt;p&gt;Keep and modernize it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this code exist only because Xamarin required it?
&lt;/h3&gt;

&lt;p&gt;Consider removing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this code solving a problem that MAUI already solves?
&lt;/h3&gt;

&lt;p&gt;Delete it.&lt;/p&gt;

&lt;p&gt;Migration is an opportunity to reduce technical debt, but &lt;strong&gt;not an excuse for a complete rewrite&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. Decouple the Business Layer
&lt;/h1&gt;

&lt;p&gt;Before migrating large amounts of UI, separate the business logic from the Xamarin-specific code.&lt;/p&gt;

&lt;p&gt;A target architecture could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Presentation
      ↓
ViewModels
      ↓
Application Services
      ↓
Domain
      ↓
Infrastructure
      ↓
Platform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MyCompany.Mobile
MyCompany.Mobile.Core
MyCompany.Mobile.Domain
MyCompany.Mobile.Application
MyCompany.Mobile.Infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The objective is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The less Xamarin-dependent your business layer is, the easier the migration becomes.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  9. PCL Migration: Don't Create Unnecessary Intermediate Work
&lt;/h1&gt;

&lt;p&gt;Many older applications use Portable Class Libraries.&lt;/p&gt;

&lt;p&gt;A common migration path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PCL
 ↓
.NET Standard
 ↓
.NET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be useful, but it should not automatically become a mandatory intermediate step.&lt;/p&gt;

&lt;p&gt;Instead, inspect what is actually inside the PCL.&lt;/p&gt;

&lt;p&gt;If it contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Models
Business Rules
Validation
API Clients
Utilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;move these into modern reusable libraries.&lt;/p&gt;

&lt;p&gt;If it contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin.Forms
Xamarin.Essentials
Platform-specific APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;separate those dependencies first.&lt;/p&gt;

&lt;p&gt;The goal is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Convert the PCL."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Extract reusable application logic from the PCL."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  10. Build the MAUI Foundation First
&lt;/h1&gt;

&lt;p&gt;Before migrating hundreds of screens, establish the new MAUI foundation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MyCompany.Mobile
│
├── Configuration
├── DependencyInjection
├── Navigation
├── Resources
│
├── Core
├── Domain
├── Application
├── Infrastructure
│
└── Platforms
    ├── Android
    ├── iOS
    ├── Windows
    └── MacCatalyst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency Injection&lt;/li&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Navigation&lt;/li&gt;
&lt;li&gt;API clients&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Storage&lt;/li&gt;
&lt;li&gt;Telemetry&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Environment management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates the platform on which the migrated features can run.&lt;/p&gt;




&lt;h1&gt;
  
  
  11. Migrate One Vertical Slice
&lt;/h1&gt;

&lt;p&gt;This is one of the most important changes to the traditional migration approach.&lt;/p&gt;

&lt;p&gt;Do not migrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Page 1
Page 2
Page 3
...
Page 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, migrate a complete business capability.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login
   ↓
Dashboard
   ↓
Customer Search
   ↓
Customer Details
   ↓
Create Order
   ↓
Submit Order
   ↓
Offline Sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single workflow exercises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI&lt;/li&gt;
&lt;li&gt;ViewModels&lt;/li&gt;
&lt;li&gt;Navigation&lt;/li&gt;
&lt;li&gt;Dependency Injection&lt;/li&gt;
&lt;li&gt;API&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Storage&lt;/li&gt;
&lt;li&gt;Offline support&lt;/li&gt;
&lt;li&gt;Platform services&lt;/li&gt;
&lt;li&gt;Telemetry&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the vertical slice works, your migration architecture has been validated.&lt;/p&gt;




&lt;h1&gt;
  
  
  12. Renderers → Handlers
&lt;/h1&gt;

&lt;p&gt;One of the major Xamarin.Forms migration areas is the transition from renderers to MAUI handlers.&lt;/p&gt;

&lt;p&gt;But don't blindly convert every renderer.&lt;/p&gt;

&lt;p&gt;Classify each renderer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Custom Renderer
      │
      ├── Styling
      │      ↓
      │    Styles
      │
      ├── Behavior
      │      ↓
      │    Behavior
      │
      ├── Property mapping
      │      ↓
      │    Handler Mapper
      │
      └── Native control
             ↓
         Custom Handler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may discover that some old renderers are no longer necessary.&lt;/p&gt;




&lt;h1&gt;
  
  
  13. Create Platform Abstractions
&lt;/h1&gt;

&lt;p&gt;Avoid spreading platform-specific code throughout ViewModels and business logic.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if ANDROID
&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;everywhere, create interfaces.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPushNotificationService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetTokenAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then implement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IPushNotificationService
        │
        ├── Android
        └── iOS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same pattern can be applied to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IBiometricsService
ILocationService
IFileService
IAudioService
IDeviceService
IBackgroundService
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps platform-specific code isolated.&lt;/p&gt;




&lt;h1&gt;
  
  
  14. Treat Native Functionality as a Separate Migration Track
&lt;/h1&gt;

&lt;p&gt;Native functionality deserves its own checklist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MainActivity
Services
Broadcast Receivers
Firebase Messaging
Permissions
Intents
Background Processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AppDelegate
Push Notifications
Background Tasks
Extensions
Permissions
Native SDKs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't consider the migration complete until these areas have been individually validated.&lt;/p&gt;




&lt;h1&gt;
  
  
  15. Firebase and Push Notifications
&lt;/h1&gt;

&lt;p&gt;Push notification migration can be more complicated than expected.&lt;/p&gt;

&lt;p&gt;Validate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App running
App backgrounded
App terminated
App restored
Token refresh
Notification tap
Deep link
Data payload
Notification payload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android
iOS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And test different OS versions and device manufacturers.&lt;/p&gt;

&lt;p&gt;Push functionality should be considered a &lt;strong&gt;production-critical capability&lt;/strong&gt;, not just another NuGet package migration.&lt;/p&gt;




&lt;h1&gt;
  
  
  16. Authentication Must Be Tested End-to-End
&lt;/h1&gt;

&lt;p&gt;Enterprise applications frequently use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth&lt;/li&gt;
&lt;li&gt;OpenID Connect&lt;/li&gt;
&lt;li&gt;Microsoft Entra ID&lt;/li&gt;
&lt;li&gt;SSO&lt;/li&gt;
&lt;li&gt;biometric authentication&lt;/li&gt;
&lt;li&gt;refresh tokens&lt;/li&gt;
&lt;li&gt;secure storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login
 ↓
Token
 ↓
API
 ↓
Token Refresh
 ↓
Logout
 ↓
Re-login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expired tokens&lt;/li&gt;
&lt;li&gt;revoked sessions&lt;/li&gt;
&lt;li&gt;offline behavior&lt;/li&gt;
&lt;li&gt;device restart&lt;/li&gt;
&lt;li&gt;application restart&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  17. Keep Xamarin and MAUI Side-by-Side
&lt;/h1&gt;

&lt;p&gt;For a very large application, a side-by-side migration can significantly reduce risk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Shared Core
                     │
          ┌──────────┴──────────┐
          ↓                     ↓
       Xamarin                MAUI
          │                     │
      Legacy UI             New UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrate functionality progressively.&lt;/p&gt;

&lt;p&gt;Eventually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin
   ↓
Remaining modules
   ↓
Zero modules
   ↓
Retire Xamarin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach allows the existing application to continue receiving releases while migration continues.&lt;/p&gt;




&lt;h1&gt;
  
  
  18. Use Feature Flags
&lt;/h1&gt;

&lt;p&gt;Feature flags allow old and new implementations to coexist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CustomerDetails.Maui
        │
        ├── false → Xamarin
        │
        └── true  → MAUI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A feature can then be enabled for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internal testers
      ↓
Pilot users
      ↓
Small production group
      ↓
Larger production group
      ↓
Everyone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a problem occurs, disable the MAUI implementation without necessarily rolling back the entire application.&lt;/p&gt;




&lt;h1&gt;
  
  
  19. CI/CD Should Be Migrated Early
&lt;/h1&gt;

&lt;p&gt;Do not wait until the application is fully converted.&lt;/p&gt;

&lt;p&gt;Build the pipeline early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pull Request
      ↓
Build
      ↓
Unit Tests
      ↓
Static Analysis
      ↓
Android Build
      ↓
iOS Build
      ↓
Integration Tests
      ↓
Artifact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Development
     ↓
QA
     ↓
UAT
     ↓
Internal
     ↓
Canary
     ↓
Production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Android signing&lt;/li&gt;
&lt;li&gt;iOS signing&lt;/li&gt;
&lt;li&gt;provisioning&lt;/li&gt;
&lt;li&gt;certificates&lt;/li&gt;
&lt;li&gt;app identifiers&lt;/li&gt;
&lt;li&gt;push configuration&lt;/li&gt;
&lt;li&gt;environment configuration&lt;/li&gt;
&lt;li&gt;crash reporting&lt;/li&gt;
&lt;li&gt;symbol upload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A migration that works only on a developer's machine is not complete.&lt;/p&gt;




&lt;h1&gt;
  
  
  20. Automated Testing Is a Migration Requirement
&lt;/h1&gt;

&lt;p&gt;Create three levels of validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business rules
Validation
Domain logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integration tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API
Authentication
Database
Synchronization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Device tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android
iOS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Critical workflows should be automated before migration begins.&lt;/p&gt;

&lt;p&gt;Otherwise, the team will spend enormous amounts of time manually comparing old and new applications.&lt;/p&gt;




&lt;h1&gt;
  
  
  21. Performance Testing
&lt;/h1&gt;

&lt;p&gt;Don't assume that moving to MAUI automatically improves performance.&lt;/p&gt;

&lt;p&gt;Compare the old and new applications.&lt;/p&gt;

&lt;p&gt;Measure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup
Memory
Navigation
Scrolling
API operations
Database operations
Synchronization
Battery usage
Application size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;low-end Android devices&lt;/li&gt;
&lt;li&gt;popular Android manufacturers&lt;/li&gt;
&lt;li&gt;supported iPhones&lt;/li&gt;
&lt;li&gt;slow networks&lt;/li&gt;
&lt;li&gt;offline mode&lt;/li&gt;
&lt;li&gt;application restart&lt;/li&gt;
&lt;li&gt;background/foreground transitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is &lt;strong&gt;measurable equivalence or improvement&lt;/strong&gt;, not simply successful compilation.&lt;/p&gt;




&lt;h1&gt;
  
  
  22. Observability During Migration
&lt;/h1&gt;

&lt;p&gt;Add migration-specific telemetry.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MigrationFeature = CustomerDetails
MigrationVersion = 5.2
Platform = Android
Implementation = MAUI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Crash rate
API failures
Navigation failures
Startup time
Feature failures
User workflow completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin production
       VS
MAUI production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;using actual data.&lt;/p&gt;




&lt;h1&gt;
  
  
  23. Don't Combine Migration With a Major Rewrite
&lt;/h1&gt;

&lt;p&gt;This is probably the most important practical recommendation.&lt;/p&gt;

&lt;p&gt;Avoid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin → MAUI
       +
New UI
       +
New architecture
       +
New API
       +
New database
       +
New authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead:&lt;/p&gt;

&lt;h3&gt;
  
  
  Release 1 — Migration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Xamarin
   ↓
MAUI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Focus on functional equivalence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Release 2 — Stabilization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAUI
 ↓
Performance
 ↓
Reliability
 ↓
Operational improvements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Release 3 — Modernization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAUI
 ↓
New UI
 ↓
Architecture improvements
 ↓
New features
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes failures easier to isolate.&lt;/p&gt;




&lt;h1&gt;
  
  
  24. Track Migration Debt
&lt;/h1&gt;

&lt;p&gt;Not everything needs to be modernized immediately.&lt;/p&gt;

&lt;p&gt;Create migration tasks such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAUI-MIG-001
Replace legacy renderer

MAUI-MIG-002
Remove Xamarin compatibility dependency

MAUI-MIG-003
Modernize navigation

MAUI-MIG-004
Replace legacy native binding

MAUI-MIG-005
Remove platform-specific conditional logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents temporary migration compromises from becoming permanent architecture.&lt;/p&gt;




&lt;h1&gt;
  
  
  25. Enterprise Migration Scorecard
&lt;/h1&gt;

&lt;p&gt;Track every module.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;Core&lt;/th&gt;
&lt;th&gt;UI&lt;/th&gt;
&lt;th&gt;Native&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Performance&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;In Progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orders&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reports&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;Complete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This creates a common language between developers, QA, architects, DevOps and management.&lt;/p&gt;




&lt;h1&gt;
  
  
  26. Recommended Migration Roadmap
&lt;/h1&gt;

&lt;p&gt;A practical enterprise migration can be organized into these phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 0
Assessment
       ↓
Phase 1
Baseline &amp;amp; Stabilization
       ↓
Phase 2
Architecture Decoupling
       ↓
Phase 3
MAUI Foundation
       ↓
Phase 4
Pilot Vertical Slice
       ↓
Phase 5
Platform &amp;amp; Native Migration
       ↓
Phase 6
Feature Migration Waves
       ↓
Phase 7
Regression &amp;amp; Performance
       ↓
Phase 8
Canary Production
       ↓
Phase 9
Full Production Rollout
       ↓
Phase 10
Xamarin Retirement
       ↓
Phase 11
Application Modernization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  27. The Golden Rules
&lt;/h1&gt;

&lt;p&gt;If I had to reduce the entire migration strategy to ten rules:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Don't start with the &lt;code&gt;.csproj&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Start with an assessment.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Establish a baseline
&lt;/h3&gt;

&lt;p&gt;Measure the existing production application.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Don't migrate everything at once
&lt;/h3&gt;

&lt;p&gt;Use incremental migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Decouple business logic
&lt;/h3&gt;

&lt;p&gt;Reduce platform dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Migrate vertical slices
&lt;/h3&gt;

&lt;p&gt;Move complete business workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Isolate native code
&lt;/h3&gt;

&lt;p&gt;Keep Android/iOS implementation behind abstractions.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Don't blindly convert renderers
&lt;/h3&gt;

&lt;p&gt;Determine whether they are still necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Use feature flags
&lt;/h3&gt;

&lt;p&gt;Keep rollback paths available.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Automate validation
&lt;/h3&gt;

&lt;p&gt;Build tests before the migration becomes large.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Separate migration from modernization
&lt;/h3&gt;

&lt;p&gt;First move to MAUI. Then improve the application.&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;A Xamarin-to-.NET MAUI migration for a large enterprise application should not be considered a simple framework upgrade.&lt;/p&gt;

&lt;p&gt;It is an &lt;strong&gt;application transformation project&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The most effective strategy is to create a stable baseline, understand the application's dependencies, separate business logic from platform-specific code, establish the MAUI foundation, and then migrate complete business capabilities incrementally.&lt;/p&gt;

&lt;p&gt;The migration should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        ASSESS
           ↓
        BASELINE
           ↓
        DECOUPLE
           ↓
      MAUI FOUNDATION
           ↓
       PILOT SLICE
           ↓
    FEATURE MIGRATION
           ↓
      VALIDATE
           ↓
     CANARY RELEASE
           ↓
    FULL PRODUCTION
           ↓
    RETIRE XAMARIN
           ↓
      MODERNIZE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important mindset shift is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't ask, "How do we convert our Xamarin application to .NET MAUI?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"How do we move our enterprise application to .NET MAUI while keeping the business running?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question leads to a very different—and much safer—migration strategy.&lt;/p&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Niladri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>xamarin</category>
      <category>maui</category>
      <category>migration</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>VideoPipeline 1.1.0: Cap the Clip, Transcode on the OS, Then Encrypt</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:11:31 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/videopipeline-110-cap-the-clip-transcode-on-the-os-then-encrypt-11d9</link>
      <guid>https://dev.to/nuvyantralabs_ccee/videopipeline-110-cap-the-clip-transcode-on-the-os-then-encrypt-11d9</guid>
      <description>&lt;p&gt;A 40-second 4K clip should not leave the device at full size. The app needs a duration cap, a smaller frame, a thumbnail, and a file it can encrypt before upload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin.Maui.VideoPipeline&lt;/strong&gt; is that path. Version &lt;strong&gt;1.1.0&lt;/strong&gt; is on nuget.org. &lt;code&gt;FromCamera&lt;/code&gt; and &lt;code&gt;FromGallery&lt;/code&gt; pick the clip. &lt;code&gt;MaxDuration&lt;/code&gt;, &lt;code&gt;MaxResolution&lt;/code&gt;, and &lt;code&gt;MaxBytes&lt;/code&gt; are the gates. Over budget, the plugin asks the OS encoder to shrink it. If the device cannot encode, the result is &lt;code&gt;CannotTranscode&lt;/code&gt;. There is no FFmpeg binary in the package.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough: how to register the defaults, run the builder, and what each platform actually transcodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What VideoPipeline is
&lt;/h2&gt;

&lt;p&gt;VideoPipeline is the video counterpart to MediaPipeline for &lt;strong&gt;.NET MAUI&lt;/strong&gt; on Android, iOS, Mac Catalyst, and Windows. MIT licensed. It targets &lt;code&gt;net10.0&lt;/code&gt;, &lt;code&gt;net10.0-android&lt;/code&gt; (API 21+), &lt;code&gt;net10.0-ios&lt;/code&gt;, &lt;code&gt;net10.0-maccatalyst&lt;/code&gt;, and &lt;code&gt;net10.0-windows10.0.17763.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It does one job: pick a video, enforce the budget, thumbnail it, and optionally encrypt it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Package&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.nuget.org/packages/Plugin.Maui.VideoPipeline" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.VideoPipeline&lt;/code&gt;&lt;/a&gt; 1.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseVideoPipeline&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entry&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;VideoPipeline.FromCamera&lt;/code&gt;, &lt;code&gt;FromGallery&lt;/code&gt;, &lt;code&gt;FromFile&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Encrypt(key)&lt;/code&gt; writes AES-256-GCM &lt;code&gt;.vault&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.VideoPipeline" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/Plugin.Maui.VideoPipeline&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-video-pipeline/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/packages/plugin-maui-video-pipeline/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Images stay on &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-media-pipeline/" rel="noopener noreferrer"&gt;Plugin.Maui.MediaPipeline&lt;/a&gt;. There is no &lt;code&gt;Current&lt;/code&gt; singleton. Tests inject a processor with &lt;code&gt;UseProcessor&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and register
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.VideoPipeline &lt;span class="nt"&gt;--version&lt;/span&gt; 1.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Plugin.Maui.VideoPipeline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMauiApp&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseVideoPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultMaxDuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DefaultMaxDuration&lt;/code&gt; applies when the builder does not call &lt;code&gt;MaxDuration&lt;/code&gt;. The options default is already 30 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick, limit, thumbnail, encrypt
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;VideoPipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromGallery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxResolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;720&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThumbnailAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Succeeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// result.Status: Cancelled, TooLarge, TooLong, CannotTranscode, Unsupported&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FromCamera()&lt;/code&gt; records. &lt;code&gt;FromFile(path)&lt;/code&gt; and &lt;code&gt;FromSource&lt;/code&gt; skip the picker. &lt;code&gt;SaveAsync&lt;/code&gt; returns &lt;code&gt;VideoPipelineResult&lt;/code&gt;. &lt;code&gt;Succeeded&lt;/code&gt; is true only for &lt;code&gt;VideoPipelineStatus.Ok&lt;/code&gt;. The artifact then carries &lt;code&gt;ThumbnailPath&lt;/code&gt; when a frame could be decoded, and &lt;code&gt;Encrypted&lt;/code&gt; when &lt;code&gt;Encrypt&lt;/code&gt; ran.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Encrypt&lt;/code&gt; takes an AES key and writes a &lt;code&gt;.vault&lt;/code&gt; file. &lt;code&gt;DecryptFileAsync&lt;/code&gt; reverses that file. &lt;code&gt;UploadWith&lt;/code&gt; accepts an &lt;code&gt;IMediaUploader&lt;/code&gt;. &lt;code&gt;StoreIn&lt;/code&gt; accepts an &lt;code&gt;IMediaVault&lt;/code&gt;. Chunked upload that must resume after process death stays on SmartUpload once you have a path.&lt;/p&gt;

&lt;p&gt;1.1.0 is the release that thumbnails and asks the OS to transcode when the clip is over budget. Android transcode honors cancel and times out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each platform encodes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Probe and thumbnail&lt;/th&gt;
&lt;th&gt;Transcode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MediaMetadataRetriever&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MediaCodec&lt;/code&gt; and &lt;code&gt;MediaMuxer&lt;/code&gt;. Many devices return &lt;code&gt;CannotTranscode&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS and Mac Catalyst&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AVAsset&lt;/code&gt; and &lt;code&gt;AVAssetImageGenerator&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AVAssetExportSession&lt;/code&gt; at 640×480 or 1280×720, with an optional duration trim.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;File size&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CannotTranscode&lt;/code&gt;, plus a copy and thumbnail floor.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;net10.0&lt;/code&gt; tests&lt;/td&gt;
&lt;td&gt;File size&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CannotTranscode&lt;/code&gt; unless a test injects &lt;code&gt;UseProcessor&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;CannotTranscode&lt;/code&gt; is a typed result. The call does not throw, and the package does not fall back to a bundled encoder. If every Android device must shrink the file, an FFmpeg binding is the tool that makes that guarantee. VideoPipeline is the path when the OS encoder is enough and a failed encode must stay a status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions the host still requests
&lt;/h2&gt;

&lt;p&gt;Android, in &lt;code&gt;Platforms/Android/AndroidManifest.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.CAMERA"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.RECORD_AUDIO"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.READ_MEDIA_VIDEO"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.READ_EXTERNAL_STORAGE"&lt;/span&gt; &lt;span class="na"&gt;android:maxSdkVersion=&lt;/span&gt;&lt;span class="s"&gt;"32"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Camera capture needs &lt;code&gt;CAMERA&lt;/code&gt; and usually &lt;code&gt;RECORD_AUDIO&lt;/code&gt;. Gallery pick on API 33+ uses &lt;code&gt;READ_MEDIA_VIDEO&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;iOS, in &lt;code&gt;Platforms/iOS/Info.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSCameraUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This app records video.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSMicrophoneUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This app records audio with video.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSPhotoLibraryUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This app reads videos from your library.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mac Catalyst and Windows use the camera and photo capabilities the MAUI host already declares.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cancelled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The picker was dismissed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TooLong&lt;/code&gt; or &lt;code&gt;TooLarge&lt;/code&gt; after transcode&lt;/td&gt;
&lt;td&gt;The OS encode still missed &lt;code&gt;MaxDuration&lt;/code&gt; or &lt;code&gt;MaxBytes&lt;/code&gt;. Lower the capture budget or reject the clip.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CannotTranscode&lt;/code&gt; on Android&lt;/td&gt;
&lt;td&gt;That device's &lt;code&gt;MediaCodec&lt;/code&gt; path could not encode. Show the status. Do not expect FFmpeg behavior.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CannotTranscode&lt;/code&gt; on Windows&lt;/td&gt;
&lt;td&gt;Windows transcode is the typed result. Probe is file size.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;code&gt;ThumbnailPath&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The platform could not decode a frame at &lt;code&gt;ThumbnailAt&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Encrypted&lt;/code&gt; is false&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Encrypt(key)&lt;/code&gt; was not on the builder.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The upload restarts after a kill&lt;/td&gt;
&lt;td&gt;VideoPipeline hands off a file. Resume belongs to SmartUpload.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where VideoPipeline sits next to the other tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Camera or gallery video, limits, thumbnail, OS transcode, encrypt&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;VideoPipeline&lt;/strong&gt; — &lt;code&gt;Plugin.Maui.VideoPipeline&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera-to-upload images&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-media-pipeline/" rel="noopener noreferrer"&gt;Plugin.Maui.MediaPipeline&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resumable upload of the file you just wrote&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-smart-upload/" rel="noopener noreferrer"&gt;Plugin.Maui.SmartUpload&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A guaranteed encode on every Android device&lt;/td&gt;
&lt;td&gt;An FFmpeg binding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MAUI &lt;code&gt;MediaPicker&lt;/code&gt; is enough when you only need the original file. VideoPipeline is the pipeline when the budget, the thumbnail, and the &lt;code&gt;.vault&lt;/code&gt; file are part of the same call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.VideoPipeline &lt;span class="nt"&gt;--version&lt;/span&gt; 1.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register &lt;code&gt;UseVideoPipeline&lt;/code&gt;, call &lt;code&gt;FromGallery&lt;/code&gt; with a 30-second cap and a 12 MB ceiling, and branch on &lt;code&gt;result.Status&lt;/code&gt;. &lt;code&gt;samples/Plugin.Maui.VideoPipeline.Sample&lt;/code&gt; covers camera, gallery, encrypt, thumbnail, and a &lt;code&gt;CannotTranscode&lt;/code&gt; result that does not crash.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.VideoPipeline" rel="noopener noreferrer"&gt;Plugin.Maui.VideoPipeline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.VideoPipeline" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/Plugin.Maui.VideoPipeline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sample: &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.VideoPipeline/tree/main/samples/Plugin.Maui.VideoPipeline.Sample" rel="noopener noreferrer"&gt;Plugin.Maui.VideoPipeline.Sample&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Package page: &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-video-pipeline/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/packages/plugin-maui-video-pipeline/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Admin&lt;/a&gt;&lt;/p&gt;

</description>
      <category>maui</category>
      <category>video</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>OfflineSync 1.0.10: Write on the Device, Sync When the Network Is Real</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:09:31 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/offlinesync-1010-write-on-the-device-sync-when-the-network-is-real-3ddk</link>
      <guid>https://dev.to/nuvyantralabs_ccee/offlinesync-1010-write-on-the-device-sync-when-the-network-is-real-3ddk</guid>
      <description>&lt;p&gt;A field note written in a dead zone has to land on the device first. The server can catch up when the radio comes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin.Maui.OfflineSync&lt;/strong&gt; is that queue. Version &lt;strong&gt;1.0.10&lt;/strong&gt; is on nuget.org. &lt;code&gt;InsertAsync&lt;/code&gt;, &lt;code&gt;UpdateAsync&lt;/code&gt;, and &lt;code&gt;DeleteAsync&lt;/code&gt; finish locally. A change log records the mutation. &lt;code&gt;SyncAsync&lt;/code&gt; pushes the log and pulls remote updates. A failed automatic push is isolated so it does not take the process down.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough: how to register the engine, write a &lt;code&gt;SyncableEntity&lt;/code&gt;, pick a conflict strategy, and what the HTTP remote expects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OfflineSync is
&lt;/h2&gt;

&lt;p&gt;OfflineSync is an offline-first sync engine for &lt;strong&gt;.NET MAUI&lt;/strong&gt; on Android and iOS. MIT licensed. It targets &lt;code&gt;net10.0&lt;/code&gt;, &lt;code&gt;net10.0-android&lt;/code&gt; (API 21+), and &lt;code&gt;net10.0-ios&lt;/code&gt; (15+). The local file is SQLite (&lt;code&gt;offlinesync.db3&lt;/code&gt; under app data) unless you pass &lt;code&gt;DatabasePath&lt;/code&gt; or &lt;code&gt;UseInMemoryStore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It does one job: keep the write on the device, then ship the change log when the network is real.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Package&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.nuget.org/packages/Plugin.Maui.OfflineSync" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.OfflineSync&lt;/code&gt;&lt;/a&gt; 1.0.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseOfflineSync&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IOfflineSyncEngine&lt;/code&gt;, or &lt;code&gt;OfflineSync.Default&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store&lt;/td&gt;
&lt;td&gt;SQLite by default, or an in-memory store for tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.OfflineSync" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/Plugin.Maui.OfflineSync&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-offline-sync/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/packages/plugin-maui-offline-sync/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Room-style insert, find, replace, and delete across SQLite, NuvexaDB, Realm, or LiteDB stays on &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-local-store/" rel="noopener noreferrer"&gt;Plugin.Maui.LocalStore&lt;/a&gt;. OfflineSync is the sync protocol, not a second CRUD API for every engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and register
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.OfflineSync &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Plugin.Maui.OfflineSync&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMauiApp&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOfflineSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoteBaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com/sync/"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConflictStrategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConflictStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastWriteWins&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoSync&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoSyncInterval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AutoSync&lt;/code&gt; is on by default, on a five-minute timer. &lt;code&gt;SyncOnNetworkRestored&lt;/code&gt; and &lt;code&gt;SyncOnResume&lt;/code&gt; are also on. Timer and connectivity sync catch exceptions. Subscribe to &lt;code&gt;SyncCompleted&lt;/code&gt; and &lt;code&gt;StatusChanged&lt;/code&gt; when the UI has to show a failed push. 1.0.10 also finishes the Android job even when &lt;code&gt;JobFinished&lt;/code&gt; arrives after the service instance is gone.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LocalOnly&lt;/code&gt; skips remote calls. &lt;code&gt;EnableBackgroundSync&lt;/code&gt; registers Android &lt;code&gt;JobScheduler&lt;/code&gt; or iOS &lt;code&gt;BGTaskScheduler&lt;/code&gt;. That flag still needs the manifest entries below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write locally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TodoItem&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SyncableEntity&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsDone&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OfflineSync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"todos"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TodoItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Buy milk"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;OfflineSync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SyncAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SyncableEntity&lt;/code&gt; carries &lt;code&gt;Id&lt;/code&gt;, &lt;code&gt;Version&lt;/code&gt;, &lt;code&gt;CreatedAtUtc&lt;/code&gt;, &lt;code&gt;UpdatedAtUtc&lt;/code&gt;, &lt;code&gt;IsDeleted&lt;/code&gt;, and a local &lt;code&gt;SyncState&lt;/code&gt;. An empty id is filled on insert. &lt;code&gt;SyncState&lt;/code&gt; stays on the device. An edit after an insert stays one insert in the change log.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;InsertAsync&lt;/code&gt;, &lt;code&gt;UpdateAsync&lt;/code&gt;, and &lt;code&gt;DeleteAsync&lt;/code&gt; complete against SQLite before any HTTP call. &lt;code&gt;GetAsync&lt;/code&gt;, &lt;code&gt;GetAllAsync&lt;/code&gt;, and &lt;code&gt;QueryAsync&lt;/code&gt; read that local set. &lt;code&gt;GetPendingCountAsync&lt;/code&gt; tells you how many changes are still waiting. &lt;code&gt;RequeueFailedAsync&lt;/code&gt; puts a &lt;code&gt;Failed&lt;/code&gt; change back on the queue after &lt;code&gt;MaxRetryAttempts&lt;/code&gt; (8 by default).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conflicts
&lt;/h2&gt;

&lt;p&gt;The default strategy is &lt;code&gt;ConflictStrategy.LastWriteWins&lt;/code&gt;: the later &lt;code&gt;UpdatedAtUtc&lt;/code&gt; is kept. &lt;code&gt;ServerWins&lt;/code&gt; keeps the remote document. &lt;code&gt;ClientWins&lt;/code&gt; keeps the local one. &lt;code&gt;Custom&lt;/code&gt; requires &lt;code&gt;OfflineSyncOptions.CustomConflictResolver&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;OfflineSync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConflictDetected&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// args.Collection, args.EntityId, args.Winner: Local, Remote, or Merged&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;StatusChanged&lt;/code&gt; reports &lt;code&gt;Idle&lt;/code&gt;, &lt;code&gt;Syncing&lt;/code&gt;, &lt;code&gt;Offline&lt;/code&gt;, or &lt;code&gt;Failed&lt;/code&gt;. &lt;code&gt;SyncCompleted&lt;/code&gt; fires after a cycle finishes, including a skip or a failure. &lt;code&gt;CollectionChanged&lt;/code&gt; fires for a local mutation and for a remote merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the HTTP remote looks like
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;HttpRemoteSyncClient&lt;/code&gt; is the default when you set &lt;code&gt;RemoteBaseAddress&lt;/code&gt;. Pull is &lt;code&gt;GET {base}/{collection}?cursor={cursor}&lt;/code&gt;. Push is &lt;code&gt;POST {base}/{collection}/changes&lt;/code&gt; with &lt;code&gt;operation&lt;/code&gt;, &lt;code&gt;baseVersion&lt;/code&gt;, &lt;code&gt;updatedAtUtc&lt;/code&gt;, and &lt;code&gt;payloadJson&lt;/code&gt;. The response lists &lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;conflicts&lt;/code&gt;, and &lt;code&gt;rejected&lt;/code&gt;. A conflict should include &lt;code&gt;serverVersion&lt;/code&gt;, &lt;code&gt;serverUpdatedAtUtc&lt;/code&gt;, and &lt;code&gt;serverPayloadJson&lt;/code&gt; so the resolver can run.&lt;/p&gt;

&lt;p&gt;Supply your own transport when the API is already shaped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IRemoteSyncClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyApiClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RemoteAccessToken&lt;/code&gt; or &lt;code&gt;RemoteAccessTokenProvider&lt;/code&gt; attaches a bearer token to the built-in client. An in-memory remote exists for the sample and for tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background sync permissions
&lt;/h2&gt;

&lt;p&gt;Leave &lt;code&gt;EnableBackgroundSync&lt;/code&gt; false until the host declares the platform entries.&lt;/p&gt;

&lt;p&gt;Android:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.RECEIVE_BOOT_COMPLETED"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.INTERNET"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.ACCESS_NETWORK_STATE"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS, with the identifier &lt;code&gt;plugin.maui.offlinesync.refresh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;BGTaskSchedulerPermittedIdentifiers&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;plugin.maui.offlinesync.refresh&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;UIBackgroundModes&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;fetch&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;processing&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When something looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OfflineSync has not been initialized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Call &lt;code&gt;UseOfflineSync&lt;/code&gt; before &lt;code&gt;OfflineSync.Default&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writes succeed and nothing reaches the server&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LocalOnly&lt;/code&gt; is true, &lt;code&gt;AutoSync&lt;/code&gt; is false and nothing called &lt;code&gt;SyncAsync&lt;/code&gt;, or the device is still offline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status stays &lt;code&gt;Failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Read &lt;code&gt;SyncCompleted&lt;/code&gt;. After &lt;code&gt;MaxRetryAttempts&lt;/code&gt;, call &lt;code&gt;RequeueFailedAsync&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ConflictStrategy.Custom&lt;/code&gt; throws at startup&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;CustomConflictResolver&lt;/code&gt;. Custom without a resolver is rejected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A failed auto-sync kills the app&lt;/td&gt;
&lt;td&gt;That crash path is what 1.0.10 isolates. Surface the error from &lt;code&gt;SyncCompleted&lt;/code&gt; instead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background sync never runs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EnableBackgroundSync&lt;/code&gt; is false, or the manifest / &lt;code&gt;Info.plist&lt;/code&gt; entries are missing.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where OfflineSync sits next to the other tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local writes, a change log, and a conflict on sync&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OfflineSync&lt;/strong&gt; — &lt;code&gt;Plugin.Maui.OfflineSync&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Room-style CRUD on the engine you pick&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-local-store/" rel="noopener noreferrer"&gt;Plugin.Maui.LocalStore&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry one HTTP call that already failed&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-retry-queue/" rel="noopener noreferrer"&gt;Plugin.Maui.RetryQueue&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable jobs with a dead-letter path&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-job-queue/" rel="noopener noreferrer"&gt;Plugin.Maui.JobQueue&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Realm and Azure Mobile Apps are the usual choice when the organization already runs that sync fabric. OfflineSync is the engine when the host wants SQLite, a small HTTP protocol, and an explicit winner for each conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.OfflineSync &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register &lt;code&gt;UseOfflineSync&lt;/code&gt; with an https base address. Insert one &lt;code&gt;SyncableEntity&lt;/code&gt;, then call &lt;code&gt;SyncAsync&lt;/code&gt; or wait for the five-minute auto-sync. &lt;code&gt;samples/Plugin.Maui.OfflineSync.Sample&lt;/code&gt; is a todo app that writes to SQLite and syncs through an in-process remote.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.OfflineSync" rel="noopener noreferrer"&gt;Plugin.Maui.OfflineSync&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.OfflineSync" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/Plugin.Maui.OfflineSync&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sample: &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.OfflineSync/tree/main/samples/Plugin.Maui.OfflineSync.Sample" rel="noopener noreferrer"&gt;Plugin.Maui.OfflineSync.Sample&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Package page: &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-offline-sync/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/packages/plugin-maui-offline-sync/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Admin&lt;/a&gt;&lt;/p&gt;

</description>
      <category>maui</category>
      <category>sync</category>
      <category>nuvyntralabs</category>
    </item>
    <item>
      <title>TlsPin 1.0.2: Reject the Call When the SPKI Pin Does Not Match</title>
      <dc:creator>NuvyntraLabs</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:06:35 +0000</pubDate>
      <link>https://dev.to/nuvyantralabs_ccee/tlspin-102-reject-the-call-when-the-spki-pin-does-not-match-1hob</link>
      <guid>https://dev.to/nuvyantralabs_ccee/tlspin-102-reject-the-call-when-the-spki-pin-does-not-match-1hob</guid>
      <description>&lt;p&gt;A payment client that accepts any certificate chain will complete the call on the wrong key. The pin has to fail that request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin.Maui.TlsPin&lt;/strong&gt; is that check. Version &lt;strong&gt;1.0.2&lt;/strong&gt; is on nuget.org. &lt;code&gt;AddTlsPin&lt;/code&gt; attaches SPKI SHA-256 hashes to one named &lt;code&gt;HttpClient&lt;/code&gt;. An empty pin set throws when you register it. A mismatch fails the request. &lt;code&gt;ReportOnly&lt;/code&gt; is the staging switch, and it still raises &lt;code&gt;OnPinFailure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough: where the pin attaches, why you ship a backup hash, and which HTTP jobs stay on the sibling plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TlsPin is
&lt;/h2&gt;

&lt;p&gt;TlsPin is the certificate-pin layer for &lt;strong&gt;.NET MAUI&lt;/strong&gt; &lt;code&gt;HttpClient&lt;/code&gt; on Android, iOS, Mac Catalyst, and Windows. MIT licensed. It targets &lt;code&gt;net10.0&lt;/code&gt;, &lt;code&gt;net10.0-android&lt;/code&gt;, &lt;code&gt;net10.0-ios&lt;/code&gt;, &lt;code&gt;net10.0-maccatalyst&lt;/code&gt;, and &lt;code&gt;net10.0-windows10.0.17763.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It does one job: compare the server public key to the hashes you configured.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Package&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.nuget.org/packages/Plugin.Maui.TlsPin" rel="noopener noreferrer"&gt;&lt;code&gt;Plugin.Maui.TlsPin&lt;/code&gt;&lt;/a&gt; 1.0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UseTlsPin&lt;/code&gt;, then &lt;code&gt;AddTlsPin&lt;/code&gt; on the client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pin&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TlsPinSet.SpkiSha256&lt;/code&gt;, base64 SHA-256 of the SPKI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.TlsPin" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/Plugin.Maui.TlsPin&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-tls-pin/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/packages/plugin-maui-tls-pin/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;UseTlsPin&lt;/code&gt; is a no-op registrar. There is no &lt;code&gt;Current&lt;/code&gt; singleton. The pin lives on the &lt;code&gt;IHttpClientBuilder&lt;/code&gt; you already use for that API. Retry, typed REST, and token refresh stay on ApiResilience, HttpForge, and SecureSession.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and pin one host
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.TlsPin &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Plugin.Maui.TlsPin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseMauiApp&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseTlsPin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTlsPin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TlsPinSet&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SpkiSha256&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;primary&lt;/code&gt; and &lt;code&gt;backup&lt;/code&gt; are base64 SHA-256 hashes of &lt;code&gt;certificate.PublicKey.ExportSubjectPublicKeyInfo()&lt;/code&gt;. &lt;code&gt;TlsPin.ComputeSpkiSha256&lt;/code&gt; produces that string from an &lt;code&gt;X509Certificate2&lt;/code&gt;. Ship at least one backup pin so a planned key rotation does not brick every install on the day the leaf changes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AddTlsPin&lt;/code&gt; throws if &lt;code&gt;Pins&lt;/code&gt; is empty, and it throws if any host set has an empty &lt;code&gt;SpkiSha256&lt;/code&gt; list. That failure is at registration, before the first request.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RequireHttps&lt;/code&gt; defaults to true. A host that does not appear in &lt;code&gt;Pins&lt;/code&gt; fails closed. &lt;code&gt;AllowUnpinnedHosts&lt;/code&gt; is the opt-in that lets an unlisted host through. Leave it false on the payments client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Report-only staging
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ReportOnly&lt;/code&gt; still calls &lt;code&gt;OnPinFailure&lt;/code&gt; and then allows the request. Use it while you confirm the hash in a build you can still talk to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTlsPin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReportOnly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnPinFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// reason is "pin mismatch" or "unpinned host"&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TlsPinSet&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SpkiSha256&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn &lt;code&gt;ReportOnly&lt;/code&gt; off before the store build. A mismatch then fails the request. &lt;code&gt;OnPinFailure&lt;/code&gt; still runs, with &lt;code&gt;"pin mismatch"&lt;/code&gt; or &lt;code&gt;"unpinned host"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Android needs &lt;code&gt;INTERNET&lt;/code&gt; if the host manifest does not already declare it. There is no pinning permission. iOS needs no extra usage string. ATS stays on because &lt;code&gt;RequireHttps&lt;/code&gt; is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TlsPin fail-closed: configure at least one host pin set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddTlsPin&lt;/code&gt; ran with an empty &lt;code&gt;Pins&lt;/code&gt; dictionary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TlsPin fail-closed: a host pin set is empty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A host entry has no &lt;code&gt;SpkiSha256&lt;/code&gt; value. Add the primary hash and a backup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every call fails after a certificate rotation&lt;/td&gt;
&lt;td&gt;The new SPKI is not in the set. The backup pin is what you rotate in before the leaf changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calls succeed in staging and fail in production&lt;/td&gt;
&lt;td&gt;Staging still has &lt;code&gt;ReportOnly = true&lt;/code&gt;. Production does not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A second host on the same client fails&lt;/td&gt;
&lt;td&gt;That host is not in &lt;code&gt;Pins&lt;/code&gt;, and &lt;code&gt;AllowUnpinnedHosts&lt;/code&gt; is false. Pin it, or give it its own named client.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You wanted retries on 401&lt;/td&gt;
&lt;td&gt;That is ApiResilience or SecureSession. TlsPin only checks the key.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where TlsPin sits next to the other tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SPKI pin on one named &lt;code&gt;HttpClient&lt;/code&gt;, fail closed&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;TlsPin&lt;/strong&gt; — &lt;code&gt;Plugin.Maui.TlsPin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A source-generated REST interface&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-httpforge/" rel="noopener noreferrer"&gt;Plugin.Maui.HttpForge&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry, circuit breaker, offline queue&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-api-resilience/" rel="noopener noreferrer"&gt;Plugin.Maui.ApiResilience&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access tokens and 401 refresh&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-secure-session/" rel="noopener noreferrer"&gt;Plugin.Maui.SecureSession&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A custom &lt;code&gt;HttpClientHandler&lt;/code&gt;, or the platform &lt;code&gt;TrustManager&lt;/code&gt; / &lt;code&gt;NSURLSession&lt;/code&gt; pin, is the usual choice when the app already owns that callback. TlsPin is the check when the hash set, the empty-set throw, and &lt;code&gt;ReportOnly&lt;/code&gt; should sit on the same &lt;code&gt;IHttpClientBuilder&lt;/code&gt; as the rest of the MAUI HTTP stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Plugin.Maui.TlsPin &lt;span class="nt"&gt;--version&lt;/span&gt; 1.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;UseTlsPin&lt;/code&gt;, then &lt;code&gt;AddTlsPin&lt;/code&gt; on the payments client with two SPKI hashes. Confirm &lt;code&gt;OnPinFailure&lt;/code&gt; in a &lt;code&gt;ReportOnly&lt;/code&gt; build, then ship with &lt;code&gt;ReportOnly&lt;/code&gt; left false. &lt;code&gt;samples/Plugin.Maui.TlsPin.Sample&lt;/code&gt; covers the public API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Plugin.Maui.TlsPin" rel="noopener noreferrer"&gt;Plugin.Maui.TlsPin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.TlsPin" rel="noopener noreferrer"&gt;github.com/nuvyntralabs/Plugin.Maui.TlsPin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sample: &lt;a href="https://github.com/nuvyntralabs/Plugin.Maui.TlsPin/tree/main/samples/Plugin.Maui.TlsPin.Sample" rel="noopener noreferrer"&gt;Plugin.Maui.TlsPin.Sample&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Package page: &lt;a href="https://nuvyntralabs.github.io/packages/plugin-maui-tls-pin/" rel="noopener noreferrer"&gt;nuvyntralabs.github.io/packages/plugin-maui-tls-pin/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By &lt;a href="https://www.linkedin.com/in/niladri-padhy-7ab41626/" rel="noopener noreferrer"&gt;Admin&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>maui</category>
      <category>tls</category>
      <category>nuvyntralabs</category>
    </item>
  </channel>
</rss>
