<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Apurv’s Sandbox]]></title><description><![CDATA[Sandboxing ideas, one stack at a time — from APIs to architecture, and everything in between]]></description><link>https://blogs.apurvghai.com</link><image><url>https://substackcdn.com/image/fetch/$s_!iXc6!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b86d944-be32-485d-9f5b-3b78a43ae97f_608x608.png</url><title>Apurv’s Sandbox</title><link>https://blogs.apurvghai.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 16 Apr 2026 14:36:04 GMT</lastBuildDate><atom:link href="https://blogs.apurvghai.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Apurv Ghai]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[apurvghai@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[apurvghai@substack.com]]></itunes:email><itunes:name><![CDATA[Apurv Ghai]]></itunes:name></itunes:owner><itunes:author><![CDATA[Apurv Ghai]]></itunes:author><googleplay:owner><![CDATA[apurvghai@substack.com]]></googleplay:owner><googleplay:email><![CDATA[apurvghai@substack.com]]></googleplay:email><googleplay:author><![CDATA[Apurv Ghai]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[GitHub Copilot CLI: When the Terminal Becomes a Thinking Partner]]></title><description><![CDATA[Transforming your terminal into a thinking partner that turns plain-language intent into safe, runnable commands.]]></description><link>https://blogs.apurvghai.com/p/github-copilot-cli-when-the-terminal</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/github-copilot-cli-when-the-terminal</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sat, 21 Feb 2026 18:13:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!yDF9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For many of us, the terminal is home. It&#8217;s where we debug production issues at 2 a.m., sift through logs, automate repetitive tasks, and build muscle memory with commands we&#8217;ve typed a thousand times. GitHub Copilot CLI isn&#8217;t here to replace that workflow it&#8217;s here to augment it in a deeply human way.</p><p>It&#8217;s my technical reflection on how bringing AI into the standard CLI changes the way we think, troubleshoot, and move fast  all without leaving the tools we already trust.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yDF9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yDF9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg 424w, https://substackcdn.com/image/fetch/$s_!yDF9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg 848w, https://substackcdn.com/image/fetch/$s_!yDF9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!yDF9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yDF9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg" width="1080" height="1350" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1350,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:228765,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blogs.apurvghai.com/i/188728115?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yDF9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg 424w, https://substackcdn.com/image/fetch/$s_!yDF9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg 848w, https://substackcdn.com/image/fetch/$s_!yDF9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!yDF9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d57162c-b819-4b03-9911-447bf84121e6_1080x1350.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>The Philosophy: Human-in-the-Loop, Not Autopilot</h2><p>What makes Copilot CLI interesting isn&#8217;t that it &#8220;knows commands.&#8221; Plenty of cheat sheets and snippets do. The difference is intent translation.</p><p>You don&#8217;t start with syntax; you start with what you want to achieve. Yep! <em><strong>Natural Language</strong></em> . Instead of switching contexts, you stay in the terminal and ask a plain-language question:</p><pre><code>&#8220;What's on my calender today?&#8221;</code></pre><p>Copilot CLI translates that intent into a concrete shell command. You review it, run it, and learn from it. That loop &#8212; human intent &#8594; machine suggestion &#8594; human judgment &#8212; is the real productivity win.</p><h2>My Experience</h2><p>Almost at the end of January, I started exploring GitHub Copilot CLI. Special thanks to <a href="https://www.linkedin.com/in/shanselman">Scott Hanselman</a> for the sparking the Copilot Energy. It has fundamentally reshaped how I think about AI as a companion. I love the CLI; I love doing async tasks, juggling multiple things, and getting into flow. Copilot CLI scaled that flow for me. I felt 5x more productive on many day-to-day tasks.</p><p><em>Why is it a game changer</em>? For me, two things stood out: the WorkIQ integration and the ability to execute shell commands safely and intelligently (including Azure CLI). I can ask Copilot CLI to review a wiki PR with my custom prompts, summarize changes, or even prepare suggested edits  then run commands to apply or validate them. Add skills.md based custom skills to the mix, and the entire ballgame changes.</p><h2>Practical Wins</h2><ul><li><p>Intent-first command generation: Ask for results in plain language; get polished, reviewable commands back.</p></li><li><p>Faster context switching: No more flipping between terminal and browser &#8212; the terminal becomes the workspace and the instructor.</p></li><li><p>Native integrations: WorkIQ, Azure CLI, and extension points mean Copilot CLI can be part of existing pipelines and workflows.</p></li><li><p>Custom skills and agents: Configure skills.md or use agent squads (see <a href="https://www.linkedin.com/in/bradygaster/">Brady Gaster&#8217;s</a> <a href="https://github.com/bradygaster/squad">Squad</a>) to automate multi-step workflows tailored to your team.</p></li></ul><p>For example, you can configure WorkIQ with custom skills to summarize your team&#8217;s conversations, prioritize what&#8217;s important, or create work items in Azure DevOps &#8212; provided you have <code>az devops</code> installed and configured. That unlocks meaningful automation across roles: developers, QA, sales, and even non-technical folks can leverage the same interface for meeting prep, monitoring, or lightweight automation.</p><h2>Resources and Community Picks</h2><ul><li><p><a href="https://github.com/bradygaster/squad">Squad (agent orchestration)</a> &#8212; a fun way to scale agent-based workflows.</p></li><li><p><a href="https://github.com/nishanil/copilot-guide">A practical guide to customizing Copilot and agents</a> &#8212; great starting point for deeper customization.</p></li></ul><p>My buddy <a href="https://www.linkedin.com/in/nanil/">Nish Anil </a>has some brilliant walkthroughs on customizing agents; if you want to tailor Copilot CLI to your daily routine, his guide is a great place to start.</p><h2>Who Benefits?</h2><p>Almost everyone. Copilot CLI can help you iterate faster, reproduce fixes, automate tedious tasks, meeting prep, summarizing materials, monitoring dashboards, drafting emails or standing up simple automations  all without memorizing complex command sequences.</p><p>If you haven&#8217;t tried GitHub Copilot CLI yet, give it a shot. Configure a couple of skills, link your <a href="https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/workiq-overview">WorkIQ</a>, and try translating a real daily task into an intent. You might be surprised how quickly it becomes a trusted thinking partner. If this post resonated, please like or share it with others and expect more updates: I&#8217;m working on new CLI skills that might help you get even more out of the tool.</p><p>Start small. Keep the human in the loop. The terminal you love just got a thinking partner.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/p/github-copilot-cli-when-the-terminal?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv&#8217;s Sandbox! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/p/github-copilot-cli-when-the-terminal?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blogs.apurvghai.com/p/github-copilot-cli-when-the-terminal?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><p></p>]]></content:encoded></item><item><title><![CDATA[.NET MAUI on iOS (macOS + Terminal): Environment Setup, First App, and Fixing Workload Errors]]></title><description><![CDATA[A step-by-step guide to installing, configuring, troubleshooting, and running your first MAUI iOS app entirely from the terminal.]]></description><link>https://blogs.apurvghai.com/p/getting-started-with-net-maui-on</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/getting-started-with-net-maui-on</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sun, 15 Feb 2026 22:36:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!OGh0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OGh0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OGh0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!OGh0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!OGh0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!OGh0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OGh0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1933213,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blogs.apurvghai.com/i/188051154?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OGh0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!OGh0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!OGh0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!OGh0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b4c13b7-cb7a-4f44-8512-52adc7ce1f66_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>If you&#8217;re a .NET developer and want to see your app running on an iPhone simulator quickly, the real work isn&#8217;t writing code &#8212; it&#8217;s getting <strong>.NET</strong>, <strong>Xcode</strong>, and <strong>the iOS simulator</strong> properly aligned.</p><p>When I started, I used Microsoft&#8217;s official MAUI first app tutorial as the baseline:<br><a href="https://dotnet.microsoft.com/en-us/learn/maui/first-app-tutorial/intro">https://dotnet.microsoft.com/en-us/learn/maui/first-app-tutorial/intro</a></p><p>This post focuses on the practical macOS setup + troubleshooting so your first build actually works &#8212; especially if you hit workload errors like I did.</p><div><hr></div><h2>Install .NET SDK (no Homebrew)</h2><pre><code><code>curl -LO https://dot.net/v1/dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh
dotnet --info
</code></code></pre><div><hr></div><h2>Install MAUI workload (the correct way)</h2><p>If you try installing <code>maui-ios</code> and see:</p><pre><code><code>Workload installation failed: Workload ID maui-ios is not recognized.
</code></code></pre><p>That&#8217;s expected in many setups. The right approach is to install the <strong>maui</strong> workload (it brings iOS pieces with it):</p><pre><code><code>dotnet workload install maui
dotnet workload list
</code></code></pre><p>If you still have issues:</p><pre><code><code>dotnet workload update
dotnet workload repair</code></code></pre><div><hr></div><h2>Install Xcode + Simulator Runtime</h2><ol><li><p>Install <strong>Xcode</strong> from the App Store</p></li><li><p>Open Xcode once and accept the license</p></li><li><p>Install command line tools:</p></li></ol><pre><code><code>xcode-select --install</code></code></pre><ol start="4"><li><p>Install an iOS simulator runtime:</p></li></ol><ul><li><p>Xcode &#8594; Settings &#8594; Platforms &#8594; install an iOS runtime</p></li><li><p>Launch Simulator once to confirm it works</p></li></ul><div><hr></div><h2>Fix Xcode location (common build breaker)</h2><p>Check current path:</p><pre><code><code>xcode-select -p</code></code></pre><p>Set it explicitly:</p><pre><code><code>sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -version</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SyOt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SyOt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png 424w, https://substackcdn.com/image/fetch/$s_!SyOt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png 848w, https://substackcdn.com/image/fetch/$s_!SyOt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png 1272w, https://substackcdn.com/image/fetch/$s_!SyOt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SyOt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png" width="1382" height="180" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:180,&quot;width&quot;:1382,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35966,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blogs.apurvghai.com/i/188051154?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SyOt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png 424w, https://substackcdn.com/image/fetch/$s_!SyOt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png 848w, https://substackcdn.com/image/fetch/$s_!SyOt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png 1272w, https://substackcdn.com/image/fetch/$s_!SyOt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bd6ea0a-7e65-411d-9b2a-01ec92ce0bb6_1382x180.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Verify:</p><pre><code><code>xcodebuild -version</code></code></pre><div><hr></div><h1>&#128640; Create Your First MAUI iOS App</h1><p>Now the fun part.</p><h2>Create the project</h2><pre><code><code>dotnet new maui -n MauiTestApp
cd MauiTestApp</code></code></pre><p>Restore:</p><pre><code><code>dotnet restore</code></code></pre><div><hr></div><h2>List Available iOS Simulators</h2><pre><code><code>xcrun simctl list devices</code></code></pre><p>Look for something like:</p><pre><code><code>iPhone 15 (Booted)
iPhone 15 Pro (Shutdown)</code></code></pre><div><hr></div><h2>Run on iOS Simulator</h2><p>Boot Simulator (if needed):</p><pre><code><code>open -a Simulator</code></code></pre><p>Build + run:</p><pre><code><code>dotnet build -t:Run -f net8.0-ios</code></code></pre><div><hr></div><h2>Stream iOS Simulator logs (super useful for &#8220;blank screen&#8221; issues)</h2><pre><code><code>xcrun simctl spawn booted log stream --style compact --level debug --predicate 'eventMessage CONTAINS[c] "&lt;your message&gt;"'
</code></code></pre><p>Replace <code>&lt;your message&gt;</code> with a keyword you log from your app.</p><div><hr></div><h2>If Build Fails</h2><h4>90% of the time:</h4><ul><li><p>Xcode path wrong</p></li><li><p>Simulator runtime missing</p></li><li><p>Workload not installed</p></li></ul><p>Check:</p><pre><code><code>dotnet workload list
xcode-select -p
xcodebuild -version</code></code></pre><div><hr></div><h1>&#128269; Viewing iOS Logs (Pro Debugging Tip)</h1><p>When app runs but something silently fails:</p><pre><code><code>xcrun simctl spawn booted log stream --style compact --level debug --predicate 'eventMessage CONTAINS[c] "&lt;your message&gt;"'</code></code></pre><p>Replace <code>&lt;your message&gt;</code> with a string you log inside your MAUI app.</p><p>This is extremely useful for:</p><ul><li><p>Blank screens</p></li><li><p>Startup crashes</p></li><li><p>Dependency injection failures</p></li><li><p>Native iOS interop issues</p></li></ul><div><hr></div><h1>Closing Thoughts</h1><p>MAUI iOS development is smooth once the toolchain is aligned:</p><ul><li><p>.NET SDK</p></li><li><p>MAUI workload</p></li><li><p>Xcode installed</p></li><li><p>Correct <code>xcode-select</code> path</p></li><li><p>Simulator runtime available</p></li></ul><p>Once those are validated, building and running from terminal becomes predictable and reliable.</p><p>Treat your environment like infrastructure. Verify it once. Then focus on your app.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/p/getting-started-with-net-maui-on?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv&#8217;s Sandbox! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/p/getting-started-with-net-maui-on?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blogs.apurvghai.com/p/getting-started-with-net-maui-on?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><p></p>]]></content:encoded></item><item><title><![CDATA[Code Signing in Dataverse — From Confusion to Confidence]]></title><description><![CDATA[Everything you need to know about proving your code&#8217;s identity in Dataverse]]></description><link>https://blogs.apurvghai.com/p/code-signing-in-dataverse-from-confusion</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/code-signing-in-dataverse-from-confusion</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sun, 07 Dec 2025 19:01:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!xxil!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>&#128272; Securing Your Dataverse Plugins: The Complete Guide to Code Signing</h4><p>Today we&#8217;ll unpack one of the most overlooked but critical steps in plugin development: <strong>code signing</strong>. Whether you&#8217;re testing locally with a self-signed certificate or preparing for production with a trusted authority, this guide walks you through both paths, so your plugins stay secure and compliant across every environment.</p><blockquote><p><strong>Update 12/10/2025 - I went ahead and updated the Microsoft Official documentation with some of this guidance. You can read <a href="https://learn.microsoft.com/en-us/power-platform/admin/set-up-managed-identity">here</a>.</strong></p></blockquote><h2>&#129517; What You&#8217;ll Learn</h2><ul><li><p>Creating and managing your signing certificate</p></li><li><p>Generating the correct subject identifier</p></li><li><p>Packaging and signing your plugins</p></li><li><p>Registering everything cleanly with Dataverse</p></li><li><p>A bonus recommendation on automating certificate signing</p></li></ul><blockquote><p>&#128161; <strong>Tip:</strong> Treat your signing certificate like a secure document. Keep it private, protected, and backed up safely.</p></blockquote><div><hr></div><h4>1&#65039;&#8419; Creating Your Digital Certificate</h4><p>Every plugin needs a verifiable identity. The certificate is your plugin&#8217;s fingerprint.<br>Here&#8217;s a sample PowerShell script to create a <strong>self-signed certificate</strong> for development and testing:</p><p>Official Example: <a href="https://learn.microsoft.com/en-us/powershell/module/pki/new-selfsignedcertificate?view=windowsserver2025-ps#example-3">https://learn.microsoft.com/en-us/powershell/module/pki/new-selfsignedcertificate?view=windowsserver2025-ps#example-3</a>.</p><pre><code><code>$params = @{
    Type = &#8216;Custom&#8217;
    Subject = &#8216;E=yourcompany@domain.com,CN=Your Company&#8217;
    TextExtension = @(
        &#8216;2.5.29.37={text}1.3.6.1.5.5.7.3.4&#8217;,
        &#8216;2.5.29.17={text}email=administrator@yourcompany.com&#8217; )
    KeyAlgorithm = &#8216;RSA&#8217;
    KeyLength = 2048
    SmimeCapabilities = $true
    CertStoreLocation = &#8216;Cert:\CurrentUser\My&#8217;
}
New-SelfSignedCertificate @params</code></code></pre><div class="pullquote"><p>&#9888;&#65039; Always use a <strong>strong password</strong>. Save this certificate in a secure location like Azure Key Vault or a secure secrets store.</p></div><p></p><blockquote><p>&#127970; <strong>For Production Environments</strong><br>The PowerShell script above works well for development and testing scenarios. However, for production deployments, <strong>it&#8217;s important to use a certificate issued by a trusted and managed certificate authority rather</strong> than relying on a self-signed certificate.</p></blockquote><div><hr></div><h4>2&#65039;&#8419; Building the Subject Identifier (The Tricky Part)</h4><p>These value ties your certificate, tenant, and environment together. You&#8217;ll need:</p><ul><li><p>Azure <strong>Tenant ID</strong></p></li><li><p>Dataverse <strong>Environment ID</strong></p></li><li><p>Certificate <strong>Subject</strong></p></li></ul><p>Here&#8217;s a reusable PowerShell helper function:</p><pre><code><code>function Get-SubjectIdentifier {
    param(
        [Parameter(Mandatory=$true)][string]$TenantId,
        [Parameter(Mandatory=$true)][string]$EnvironmentId,
        [Parameter(Mandatory=$true)][string]$Subject
    )

    $bytes = [Guid]::Parse($TenantId).ToByteArray()
    $encodedTenant = [Convert]::ToBase64String($bytes).TrimEnd(&#8217;=&#8217;).Replace(&#8217;+&#8217;,&#8217;-&#8217;).Replace(&#8217;/&#8217;,&#8217;_&#8217;)
    $encodedSubject = [System.Web.HttpUtility]::UrlEncode($Subject)

    return &#8220;/eid1/c/pub/t/$encodedTenant/a/qzXoWDkuqUa3l6zM5mM0Rw/n/plugin/e/$EnvironmentId/i/$encodedSubject/s/$encodedSubject&#8221;
}
</code></code></pre><div><hr></div><h4>3&#65039;&#8419; Packaging and Signing</h4><p>Now, time to seal the deal.</p><p>Package your plugin:</p><pre><code><code>nuget spec YourPlugin.dll
nuget pack YourPlugin.nuspec</code></code></pre><p>Sign it with your certificate:</p><pre><code><code>nuget sign YourPlugin.nupkg `
    -CertificatePath plugin-signing.pfx `
    -CertificatePassword &#8220;YourSecretPassword&#8221; `
    -Timestamper http://timestamp.digicert.com
</code></code></pre><p>Timestamping ensures your signature remains valid even after your certificate expires.</p><blockquote><p>&#128293;  <strong>If you want to automate your certificate generation and integrate signing into your DevOps or GitHub Actions pipeline, I highly recommend reading <a href="https://www.linkedin.com/in/shanselman/">Scott Hanselman&#8217;s</a> blog on </strong><em><strong><a href="https://www.hanselman.com/blog/automatically-signing-a-windows-exe-with-azure-trusted-signing-dotnet-sign-and-github-actions/">Automatically Signing a Windows EXE with Azure Trusted Signing, dotnet sign, and GitHub Actions</a></strong></em><strong>.</strong><br>The same concepts apply directly to Dataverse plugin signing and help you shift from manual PFX handling to a secure, cloud-based signing workflow.</p></blockquote><div><hr></div><h4>4&#65039;&#8419; Registering / Update Managed Identity with Dataverse</h4><p>To understand how to create or update the identity record with the correct attributes, read:</p><blockquote><p>&#129513; <strong>Goodbye Secrets &#8212; Using Managed Identity in Dataverse Plugins.</strong> <a href="https://blogs.apurvghai.com/p/goodbye-secrets-using-managed-identity">https://blogs.apurvghai.com/p/goodbye-secrets-using-managed-identity </a></p></blockquote><div><hr></div><p>Finally, here&#8217;s the flow:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xxil!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xxil!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png 424w, https://substackcdn.com/image/fetch/$s_!xxil!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png 848w, https://substackcdn.com/image/fetch/$s_!xxil!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png 1272w, https://substackcdn.com/image/fetch/$s_!xxil!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xxil!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png" width="567" height="1010" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1010,&quot;width&quot;:567,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:567931,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blogs.apurvghai.com/i/178382258?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xxil!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png 424w, https://substackcdn.com/image/fetch/$s_!xxil!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png 848w, https://substackcdn.com/image/fetch/$s_!xxil!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png 1272w, https://substackcdn.com/image/fetch/$s_!xxil!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F272ba2ba-318b-4ec8-a9d2-6d34df6fbd23_567x1010.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">caption...</figcaption></figure></div><p></p><h4>&#128269; Troubleshooting Quick Wins</h4><p><strong>Certificate not recognized</strong></p><ul><li><p>Verify that the subject identifier is encoded correctly.</p></li><li><p>Make sure the tenant ID was converted using Base64URL (no padding, no &#8220;+&#8221; or &#8220;/&#8221;).</p></li><li><p>Confirm that the subject extracted from the certificate matches what you passed during registration.</p></li></ul><p><strong>Signing errors during </strong><code>nuget sign</code></p><ul><li><p>Ensure the certificate includes <strong>Digital Signature</strong> and <strong>Code Signing</strong> key usages.</p></li><li><p>Check that the certificate is valid (not expired or missing private key).</p></li><li><p>If using a PFX, confirm the password is correct and the file contains the full chain.</p></li></ul><p><strong>Plugin package won&#8217;t load in Dataverse</strong></p><ul><li><p>Verify the NuGet package is signed and timestamped.</p></li><li><p>Ensure the certificate (or managed identity entry) is registered in the <strong>Plugin Package </strong> table.</p></li><li><p>Check the Dataverse plugin trace logs for &#8220;signature validation&#8221; errors.</p></li></ul><p><strong>Subject identifier rejected by CLI or Dataverse</strong></p><ul><li><p>Confirm tenant and environment IDs are correct GUIDs.</p></li><li><p>Validate encoding by regenerating with the script (Python or PowerShell).</p></li><li><p>Ensure no accidental spaces or hidden characters were added when copying.</p></li></ul><div><hr></div><h4>&#9989; Best Practices Checklist</h4><ul><li><p>Use descriptive certificate subjects (e.g., &#8220;CN=Plugin Signing &#8211; Prod&#8221;)</p></li><li><p>Store certs in Azure Key Vault, not on local disks</p></li><li><p>Document your identifiers for each environment</p></li><li><p>Always use a trusted timestamp server</p></li><li><p>Automate these steps in your DevOps pipeline</p></li></ul><div><hr></div><h4>&#129504; Additional Resources</h4><ul><li><p><a href="https://learn.microsoft.com/power-apps/developer/data-platform/register-plug-in">Dataverse Plugin Registration Docs</a></p></li><li><p><a href="https://learn.microsoft.com/power-platform/developer/cli/introduction">Power Platform CLI Reference</a></p></li><li><p><a href="https://learn.microsoft.com/en-us/power-platform/admin/managed-identity-overview">Azure Managed Identity + Dataverse Plug-in Integration</a></p></li></ul><div><hr></div><blockquote><p>&#127775; Taking time to properly sign your code builds trust &#8212; for your environment, your team, and your future self.</p></blockquote><p>Happy building, and see you in the traces! &#128640;</p>]]></content:encoded></item><item><title><![CDATA[Goodbye Secrets — Using Managed Identity in Dataverse Plugins.]]></title><description><![CDATA[Learn how to securely connect your Dataverse plugins and custom APIs to Azure resources using Managed Identity &#8212; without touching a single client secret.]]></description><link>https://blogs.apurvghai.com/p/goodbye-secrets-using-managed-identity</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/goodbye-secrets-using-managed-identity</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sat, 08 Nov 2025 23:02:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zkNB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>&#128272; <strong>Still relying on client secrets in your plugins?</strong> </h3><p>You&#8217;re not alone. One expires&#8212;buried deep in config or environment settings&#8212;and suddenly, you&#8217;re firefighting downtime. You patch it, it works&#8230; until the next time.</p><p>There&#8217;s a better way: <strong>Managed Identity (MI)</strong> with <strong>Federated Identity Credentials (FIC)</strong>. No secrets. No rotation. No surprises.</p><p>Let me show you how to set it up&#8212;step by step.</p><div><hr></div><h4>&#9881;&#65039; What You&#8217;ll Need</h4><ul><li><p>Azure subscription (for creating a User Managed Identity &#8212; UMI)</p></li><li><p>Dataverse environment where your plugin or Custom API runs</p></li><li><p>System Administrator role (to create Managed Identity records)</p></li><li><p>Entra ID  App Registration (to hold the FIC)</p></li><li><p>XrmToolBox with the <strong>REST Builder</strong> plugin (to simplify record creation)</p></li></ul><p>&#128216; Reference: Microsoft Learn &#8212; <a href="https://learn.microsoft.com/en-us/power-platform/admin/set-up-managed-identity">Set up Managed Identity in Dataverse</a></p><div><hr></div><h4>&#129706; Step 1: Create a User Managed Identity in Azure</h4><h5>Run:</h5><pre><code><code>az identity create --name dataverse-plugin-mi --resource-group my-rg</code></code></pre><p>Note down:</p><ul><li><p>Client ID</p></li><li><p>Object ID</p></li><li><p>Tenant ID</p></li></ul><p>You&#8217;ll use them in Dataverse next.</p><div><hr></div><h4>&#128450;&#65039; Step 2: Register the Managed Identity in Dataverse</h4><p>I find <strong>XrmToolBox &#8594; REST Builder</strong> much easier to work with :)</p><ol><li><p>Connect to your Dataverse environment</p></li><li><p>Open Dataverse REST Builder &#8594; choose <code>POST/CREATE</code> &#8594; entity name <code>managedidentity. </code></p></li><li><p>Select the columns and provide the values as below:</p></li></ol><pre><code><code>{
  "applicationid" : "&lt;applicationId, or ClientId on Entra", //I recommend using User Managed Identity
  "credentialsource" : "2" // Managed client,
  "subjectscope" : "1" // You can use devOnlyScope usually you would use EnvironmentScope
  "tenantid" : "provide your tenant Id"
}</code></code></pre><ol start="4"><li><p>Click <strong>Execute</strong> &#8594; the record is created</p></li></ol><p>&#128161; REST Builder automatically handles OAuth headers.</p><p>I always recommend reviewing the EntityType Schema. Here&#8217;s the link for <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/managedidentity?view=dataverse-latest">ManagedIdentity</a> Table.</p><p>To learn how to use Dataverse Rest Builder, review the <a href="https://github.com/GuidoPreite/DRB?tab=readme-ov-file">project website</a>. Personally, I love XrmToolbox because of how easy it is to use. But you can always <a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/power-fx#create-dataverse-records">create records using Power Platform CLI</a>.</p><blockquote><p>If you are planning to use <strong>Power Platform CLI,</strong> do make sure to setup authentication. That will make it easier to interact with CLI. Here&#8217;s the <a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/auth">link</a> for pac auth command.</p></blockquote><div><hr></div><h4>&#128279; Step 3: Add a Federated Identity Credential (FIC)</h4><p>The FIC links Microsoft Entra ID, your plugin, and the Managed Identity.</p><p>You can:</p><ul><li><p>Add it manually in the Azure Portal &#8594; App Registration &#8594; Certificates &amp; Secrets &#8594; Federated Credentials</p></li><li><p>Or automate it with PowerShell:</p></li></ul><pre><code><code>az ad app federated-credential create `
  --id &lt;app-registration-client-id&gt; `
  --parameters fic.json
</code></code></pre><h4>&#9881;&#65039; Automate FIC Creation (optional)</h4><pre><code><code>param(
    [string]$AppId,
    [string]$TenantId,
    [string]$subjectIdentifier,
    [string]$Name = &#8220;dataverse-fic&#8221;
)
az login
$json = @&#8221;
{
  &#8220;name&#8221;: &#8220;$Name&#8221;,
  &#8220;issuer&#8221;: &#8220;https://sts.windows.net/$TenantId/&#8221;,
  &#8220;subject&#8221;: $subjectIdentifier,
  &#8220;audiences&#8221;: [&#8221;api://AzureADTokenExchange&#8221;]
}
&#8220;@
$tmp = New-TemporaryFile
$json | Out-File $tmp
az ad app federated-credential create --id $AppId --parameters @$tmp
Remove-Item $tmp
</code></code></pre><p>Run once per environment. Example <code>fic.json</code> that you can create and use with Azure CLI. </p><p>Here&#8217;s the <a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/auth">official documentation</a> to understand the <strong>Subject Identifier</strong>. </p><pre><code><code>{
  "name": "dataverse-fic",
  &#8220;issuer&#8221;: &#8220;https://login.microsoftonline.com/{tenantID}/v2.0&#8221;,
  &#8220;subject&#8221;: &#8220;&lt;generate subject identifier&gt;&#8221;,
  &#8220;audiences&#8221;: [&#8221;api://AzureADTokenExchange&#8221;]
} </code></code></pre><blockquote><p>&#128680; Heads-up: the next post dives into plugin signing with NuGet&#8212;don&#8217;t miss it.</p></blockquote><div><hr></div><h4>&#129513; Step 4 &#8211; Associate the Managed Identity Record with Your Plugin Assembly or Package</h4><p>At this point you have:</p><ol><li><p>A <strong>User Managed Identity</strong> in Azure</p></li><li><p>A <strong>Managed Identity record</strong> in Dataverse (created in Step 2)</p></li><li><p>A <strong>plugin assembly</strong> already registered</p></li></ol><p>But Dataverse still doesn&#8217;t know <strong>which</strong> plugin assembly should use <strong>which</strong> managed identity.</p><p>We&#8217;ll do this with <strong>XrmToolBox &#8594; REST Builder</strong>.</p><h3>What we&#8217;re doing</h3><p>We will send:</p><ul><li><p><code>PATCH/UPDATE /api/data/v9.0/pluginassemblies(&lt;PluginAssemblyId&gt;)</code></p></li><li><p>Body:</p></li></ul><pre><code><code>{
  &#8220;managedidentityid@odata.bind&#8221;: &#8220;/managedidentities(&lt;ManagedIdentityGuid&gt;)&#8221;
}
</code></code></pre><p>That single PATCH is what turns on managed identity for that plugin. Follow the steps below:</p><ul><li><p>Open <strong>XrmToolBox</strong> and connect to your Dataverse environment.</p></li><li><p>Open <strong>REST Builder</strong>.</p></li><li><p>In the top, select:</p><ul><li><p>Method: <code>UPDATE</code></p></li><li><p>URL / Entity: <code>pluginassemblies(&lt;PluginAssemblyId&gt;)</code></p></li></ul></li><li><p>To get <code>&lt;PluginAssemblyId&gt;</code>:</p><ul><li><p>Use REST Builder first with a <code>GET pluginassemblies?$select=pluginassemblyid,name</code> to find it.</p></li></ul></li><li><p>In the <strong>Body</strong> section, you will see below once your have provided your managed identity ID:</p></li></ul><pre><code><code>{
  &#8220;managedidentityid@odata.bind&#8221;: &#8220;/managedidentities(&lt;ManagedIdentityGuid&gt;)&#8221;
}
</code></code></pre><ul><li><p>Replace <code>&lt;ManagedIdentityGuid&gt;</code> with the GUID of the managed identity record you created in Step 2. (You can get this the same way &#8212; <code>GET managedidentities?$select=managedidentityid</code> in REST Builder, or just run that in your browser if you are logged in to the organization.)</p></li><li><p>Click <strong>Execute</strong>.</p></li><li><p>If the PATCH succeeds, your plugin assembly now has a managed identity bound to it. </p></li></ul><div><hr></div><h4>&#129504; Step 5: Use <code>IManagedIdentityService</code> in Your Plugin or Custom API</h4><p>Now your plugin can request a token for any external API securely.</p><pre><code><code>var miService = (IManagedIdentityService)serviceProvider.GetService(typeof(IManagedIdentityService));
var token = await miService.GetAccessTokenAsync(&#8221;https://api.coolapi.com/.default&#8221;);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(&#8221;Bearer&#8221;, token);
</code></code></pre><p>That&#8217;s it! No secrets, no rotation.</p><div><hr></div><h4>&#128202; Step 6: Monitor the Flow</h4><p><strong>Plugin Trace Logs</strong></p><ul><li><p>Enable &#8220;All&#8221; logging under Plugin Trace Settings.</p></li><li><p>Add logger calls like <code>logger.LogInformation(&#8221;Requesting token...&#8221;);</code></p></li><li><p>Filter for <code>YourPluginType</code> to confirm activity</p></li></ul><blockquote><p>I highly recommend enabling application insights. You can read more about it <a href="https://learn.microsoft.com/en-us/power-platform/admin/overview-integration-application-insights">here</a>.</p></blockquote><p><strong>Application Insights</strong></p><ul><li><p>Enable dependency tracking</p></li><li><p>Use a Kusto query like:</p></li></ul><pre><code><code>dependencies
| where target contains &#8220;api.coolapi.com&#8221;
| order by timestamp desc
</code></code></pre><p>Two entries (<code>login.microsoftonline.com</code> and your API) mean token and call both succeeded.</p><div><hr></div><h4>&#129520; Troubleshooting </h4><p><strong>FIC missing</strong> &#8594; Token request fails &#8594; Create FIC in App Registration<br><strong>FIC mismatch</strong> &#8594; 401 or null token &#8594; Ensure <code>subject</code> matches the requirements.<br><strong>Unauthorized</strong> &#8594; Wrong audience &#8594; Use <code>https://api.coolapi.com/.default</code><br><strong>External API denied</strong> &#8594; Token works but API rejects &#8594; Grant the UMI permissions in Azure or Easy Auth. </p><p>&#129504; Check Microsoft Entra ID AD &#8594; Sign-in Logs &#8594; filter by App Registration to see failed token exchanges.</p><div><hr></div><h4>&#129514; Behind the Scene</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zkNB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zkNB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png 424w, https://substackcdn.com/image/fetch/$s_!zkNB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png 848w, https://substackcdn.com/image/fetch/$s_!zkNB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png 1272w, https://substackcdn.com/image/fetch/$s_!zkNB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zkNB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png" width="955" height="511" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:511,&quot;width&quot;:955,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:36303,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://apurvghai.substack.com/i/177821260?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zkNB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png 424w, https://substackcdn.com/image/fetch/$s_!zkNB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png 848w, https://substackcdn.com/image/fetch/$s_!zkNB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png 1272w, https://substackcdn.com/image/fetch/$s_!zkNB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd8600a55-71d6-45f8-9c9b-d4781d7d8dd9_955x511.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>&#128680; Quick Checklist</h2><ul><li><p>Managed Identity record exists in Dataverse</p></li><li><p>Audience is <code>api://AzureADTokenExchange</code></p></li><li><p>Plugin trace shows token retrieval</p></li><li><p>External API returns 200 OK</p></li><li><p>App Insights shows dependency telemetry</p></li></ul><div><hr></div><h2>&#129534; Summary</h2><p>1&#65039;&#8419; Create User Managed Identity in Azure.<br>2&#65039;&#8419; Register it in Dataverse (via REST Builder).<br>3&#65039;&#8419; Add Federated Credential in App Registration.<br>4&#65039;&#8419; Use <code>IManagedIdentityService</code> for token access.<br>5&#65039;&#8419; Monitor with trace logs and/or App Insights.<br>6&#65039;&#8419; Troubleshoot with FIC and Azure logs.</p><div><hr></div><h2>&#127919; Final Thoughts</h2><p>Most tutorials end when the code compiles.<br>In production, the real success is <strong>visibility</strong> &#8212; knowing when the token request worked and when it didn&#8217;t.</p><p>By combining:</p><ul><li><p>Microsoft&#8217;s official setup steps</p></li><li><p>XrmToolBox REST Builder for speed</p></li><li><p>Application Insights for observability</p></li><li><p>Clear FIC troubleshooting patterns</p></li></ul><p>&#8230;you&#8217;ll build secure, secretless integrations that actually last.</p><p>&#128172; Enjoyed this guide? Subscribe for more Power Platform deep dives &#8212; covering Dataverse extensibility, telemetry, and real-world supportability engineering.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv&#8217;s Sandbox! Subscribe for free to receive new posts.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[🚗 A Pothole, a Dashboard Light, and a Reminder to Stay Curious ]]></title><description><![CDATA[Not every problem needs to be solved with the first idea that comes to mind.]]></description><link>https://blogs.apurvghai.com/p/a-pothole-a-dashboard-light-and-a</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/a-pothole-a-dashboard-light-and-a</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Tue, 04 Nov 2025 03:15:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iXc6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b86d944-be32-485d-9f5b-3b78a43ae97f_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Not every problem needs to be solved with the first idea that comes to mind.</p><p>The other day, I hit a pothole. Nothing unusual there. We&#8217;ve all been there. But right after that bump, my windshield washer fluid light lit up on the dashboard. It wasn&#8217;t a big deal at first, until I noticed something else.</p><p>That light was now blocking my trip meter. And for some reason, that really bugged me.</p><p>So what&#8217;s the obvious thing to do?</p><p>Just add more windshield fluid, right? That&#8217;s probably what most people would try. But that&#8217;s not what I did.</p><h4><strong>Curiosity First</strong></h4><p>Before jumping into a fix, I wanted to understand what was actually going on. That&#8217;s where real troubleshooting begins.</p><ul><li><p>Was the washer fluid actually low?</p></li><li><p>Were the wipers still working?</p></li><li><p>Was there still water in the reservoir?</p></li><li><p>And why on earth would hitting a pothole cause this in the first place?</p></li></ul><p>That question stuck with me. Something didn&#8217;t add up. I started thinking maybe it wasn&#8217;t the fluid at all. Maybe it was a sensor. Maybe a connector came loose from the impact. Could I reach it? Could I fix it myself?</p><h4><strong>A Hands-On Investigation</strong></h4><p>So I did what any curious person would do. I got under the car to take a look.</p><p>Sure enough, I found the sensor. But like most car parts, it wasn&#8217;t exactly easy to get to. I would have to remove the underbody mat to see more clearly, and that meant dealing with those tiny plastic pins that tend to snap unless you&#8217;ve got extras. I didn&#8217;t have any on hand.</p><p>I took a good look at the sensor and its wiring. Everything looked intact, although the reservoir was slightly cracked. I tried adjusting the connector wire a bit. Still, I topped off the fluid just in case. Nothing changed. The light stayed on.</p><p>At that point, I was pretty confident the sensor had gone bad. Maybe it was already wearing out and the pothole just gave it a final nudge. Either way, it wasn&#8217;t reading correctly.</p><p>So I went to a local shop and had them disconnect it for now. The light disappeared and I got my trip meter back.</p><h4><strong>More Than Just a Fix</strong></h4><p>Here&#8217;s the thing. I didn&#8217;t actually fix the root issue. But I understood what caused it. And that was enough.</p><p>For me, the value wasn&#8217;t in pouring more fluid or replacing the sensor right away. It was in breaking down the problem and thinking it through step by step. That&#8217;s what good troubleshooting looks like.</p><p>It&#8217;s not about throwing a quick fix at the issue. It&#8217;s about asking the right questions, ruling things out, testing a few theories, and getting to the root of what&#8217;s really going on.</p><h4><strong>The Takeaway</strong></h4><p>Whether it&#8217;s a broken build, a leaky pipe, or a warning light on your dashboard, the mindset is the same.</p><p>Start by being curious. Not rushed.<br>Don&#8217;t assume. Investigate.</p><p>Because when you take the time to understand the problem, the solution usually finds its way to you.</p><p>Next time something goes wrong, don&#8217;t just fix it. Ask why it happened in the first place. That one question can open up a whole new way of seeing the world &#8212; and maybe even make you enjoy the problem-solving process a little more.</p>]]></content:encoded></item><item><title><![CDATA[🔧 Troubleshooting - Go Far by Going Deep ]]></title><description><![CDATA[It&#8217;s about understanding the problem at hand.]]></description><link>https://blogs.apurvghai.com/p/troubleshooting-go-far-by-going-deep</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/troubleshooting-go-far-by-going-deep</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sun, 02 Nov 2025 02:46:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iXc6!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b86d944-be32-485d-9f5b-3b78a43ae97f_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Troubleshooting isn&#8217;t just about fixing things. It&#8217;s about understanding the problem at hand.</p><p>That&#8217;s something I&#8217;ve learned again and again&#8212;in code, in systems, and even in everyday life. The instinct to pause, observe, and go deeper is what separates a quick patch from a real resolution.</p><h4><strong>A Tech Example</strong></h4><p>Your build pipeline fails. Maybe your <code>.NET</code> project doesn&#8217;t compile on Azure DevOps. The natural instinct is to think, <em>&#8220;It must be a cloud agent issue.&#8221;</em> But wait&#8212;does it fail on your local machine too?</p><p>If not, the issue isn&#8217;t your code. It&#8217;s something about the hosted environment. If it does fail locally, now you&#8217;re on to something. Could it be a missing package, a bad path, a wrong version of the SDK?</p><p>You start peeling layers. You recreate the steps. You mimic the environment. You compare logs, configurations, build parameters. You go step by step.</p><p>That&#8217;s troubleshooting. It&#8217;s not about rushing to the answer&#8212;it&#8217;s about understanding the system, its state, and where it diverges from what you expect.</p><div><hr></div><h4><strong>A Real-World Example</strong></h4><p>Let&#8217;s say your kitchen sink is pooling water. First reaction? Go buy some chemical drain cleaner and dump it in. But that&#8217;s not troubleshooting&#8212;that&#8217;s trial-and-error with a price tag.</p><p>Instead, stop and ask: Why is the water pooling? Where&#8217;s the trap? Is it blocked? What&#8217;s the principle of how this pipe system works?</p><p>You take a wrench. You place a bowl underneath. You loosen the trap. You inspect. Maybe it&#8217;s food scraps or grease buildup. You clean it, reassemble, test&#8212;and now you not only solved it, but you understand how it works. You&#8217;ve learned something.</p><p>That&#8217;s what matters. Not just solving, but understanding.</p><div><hr></div><h4><strong>Be Methodical</strong></h4><p>When you&#8217;re troubleshooting anything, it&#8217;s critical to be methodical. Start with a few simple questions:</p><ul><li><p>What exactly is the problem?</p></li><li><p>When did it start happening?</p></li><li><p>What changed?</p></li><li><p>Can I isolate it? Can I reproduce it?</p></li></ul><p>You don&#8217;t panic. You don&#8217;t jump to the most complex fix. You walk through it. You verify assumptions. You understand.</p><p>Because once you understand a system&#8212;really understand it&#8212;the solution often reveals itself. It&#8217;s not magic. It&#8217;s not guesswork. It&#8217;s clarity.</p><div><hr></div><blockquote><p><strong>The Mindset</strong></p></blockquote><p>This mindset&#8212;to troubleshoot, to explore, to be curious&#8212;applies to everything. A flaky Wi-Fi connection. A sudden drop in app performance. A teammate who seems off.</p><p>It all starts with a willingness to go one level deeper, to ask, what&#8217;s really going on here?</p><div><hr></div><p>If you want to go far&#8212;in engineering, in leadership, in life&#8212;learn to go one step forward. Don&#8217;t rush past the problem. Sit with it. Study it.</p><p>Understanding is the path. Curiosity is the spark.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv&#8217;s Sandbox! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Welcome to My World of Troubleshooting]]></title><description><![CDATA[What Does Troubleshooting Mean to Me?]]></description><link>https://blogs.apurvghai.com/p/welcome-to-my-world-of-troubleshooting</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/welcome-to-my-world-of-troubleshooting</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sun, 02 Nov 2025 02:45:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iXc6!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b86d944-be32-485d-9f5b-3b78a43ae97f_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello! I&#8217;m Apurv, and I really enjoy solving problems&#8212;big or small.</p><h4><strong>What Does Troubleshooting Mean to Me?</strong></h4><p>For me, troubleshooting is so much more than just following a manual. It&#8217;s about getting curious, digging deep into problems, asking questions others might not think of, and trying to understand why things work&#8212;or why they suddenly stop working.</p><p>Whether it&#8217;s a technical issue at work, a warning light in my car, or a stubborn kitchen sink, I like to approach every challenge with curiosity, a clear method, and a refusal to settle for quick fixes.</p><h4><strong>About This Series</strong></h4><p>This post is the beginning of a new series where I&#8217;ll share how I think, solve, debug, and yes, sometimes even make mistakes. I want this to be for everyone&#8212;whether you work in technology or just enjoy the satisfaction of figuring things out.</p><h4><strong>What You Can Expect</strong></h4><p>Here&#8217;s what I&#8217;ll be writing about:</p><ul><li><p>Real-world tech debugging stories, including some from systems you might use every day</p></li><li><p>Everyday problems at home that I&#8217;ve solved (and what I learned along the way)</p></li><li><p>The mindset that turns frustration into fascination</p></li></ul><p>No fluff. No clickbait. Just honest stories and practical strategies about one of the most valuable skills out there: troubleshooting.</p><div><hr></div><p>Stay tuned, and let&#8217;s explore and solve mysteries together, one problem at a time.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv&#8217;s Sandbox! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Say Goodbye to Expired Secrets with Federated Identity Credentials ]]></title><description><![CDATA[Streamline App Identity with Managed Identity and FIC]]></description><link>https://blogs.apurvghai.com/p/say-goodbye-to-expired-secrets-with</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/say-goodbye-to-expired-secrets-with</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sun, 02 Nov 2025 02:44:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!fZEx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello &#128075;! As developers, we&#8217;ve all been there that frantic call or notification that an application is down because a secret or certificate has expired. It&#8217;s a classic problem and a major pain point in the development lifecycle. What if I told you there&#8217;s a better way to manage your app&#8217;s credentials, a way that lets you ditch those pesky secrets and certificates for good?</p><p>That&#8217;s where <strong>Federated Identity Credentials (FIC)</strong> come to the rescue! This feature in <a href="https://docs.azure.cn/en-us/entra/fundamentals/what-is-entra">Microsoft Entra ID</a> (formerly AAD) lets your applications authenticate to Azure resources without needing to store any secrets or certificates. It&#8217;s a game-changer for improving security and reducing operational headaches.</p><blockquote><p><a href="https://learn.microsoft.com/en-us/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0">Deep dive on FIC in Microsoft Learn &#8594;</a></p></blockquote><p><strong>What is a Federated Identity Credential?</strong></p><p>In our case, it&#8217;s about trusting a workload (like a managed identity) to authenticate as a specific AAD app registration. Instead of using a secret or certificate, the workload presents a token to AAD, which verifies the trust and issues an access token for the app registration.</p><p>This means your application, now armed with this new access token, can authenticate to other Azure resources without a hitch. It&#8217;s a secure, seamless, and secret-free way to manage your app&#8217;s identity.</p><div><hr></div><p><strong>Our Scenario: Ditching Secrets with a Managed Identity</strong></p><p>Let&#8217;s walk through a practical example of how to implement this. We&#8217;ll use a <strong>User-Assigned Managed Identity (UAMI)</strong> to federate with an Azure App Registration. This UAMI will then be assigned to an Azure App Service, allowing our application to authenticate to other Azure resources.</p><p>Here&#8217;s the flow we&#8217;ll set up:</p><ul><li><p><strong>Create an App Registration:</strong> This will be the main identity of our application.</p></li><li><p><strong>Add a Federated Credential:</strong> We&#8217;ll add a FIC to the app registration, linking it to our UAMI.</p></li><li><p><strong>Assign the UAMI to the App Service:</strong> The App Service will now use the UAMI.</p></li></ul><p>Now, let&#8217;s dive into the details.</p><h4><strong>Step 1: Create an App Registration</strong></h4><p>First, you&#8217;ll need an App Registration in your Azure Active Directory. This is the application&#8217;s identity. Give it a name, and that&#8217;s it! No need to create any secrets or certificates here.</p><h4><strong>Step 2: Add the Federated Identity Credential</strong></h4><p>This is where the magic happens. Navigate to your App Registration, go to the Certificates &amp; secrets blade, and click on the Federated credentials tab. Here, you&#8217;ll add a new credential.</p><ul><li><p><strong>Scenario:</strong> Select Azure services</p></li><li><p><strong>Service:</strong> Choose Managed identity</p></li><li><p><strong>Managed identity:</strong> Select the User-Assigned Managed Identity you want to federate with.</p></li></ul><h4><strong>Step 3: Assign the Managed Identity to the App Service</strong></h4><p>Now, go to your App Service, navigate to the Identity blade, and select the User assigned tab. Add the same UAMI you used in the previous step.</p><p>That&#8217;s it! Your App Service is now configured to use the UAMI, which has a federated trust with your App Registration.</p><h4><strong>How the Authentication Flow Works</strong></h4><p>Now that everything is set up, let&#8217;s understand the flow:</p><ol><li><p>When your application running on the App Service needs to authenticate, it uses the User-Assigned Managed Identity.</p></li><li><p>The UAMI requests a token from Azure AD, which verifies its identity.</p></li><li><p>The UAMI then uses a special token as an assertion to acquire a new access token for the App Registration. This is the core of the federated identity.</p></li><li><p>Your application, now holding the App Registration&#8217;s access token, can securely call other Azure services or APIs that are configured to accept this identity.</p></li></ol><p>This entire process happens seamlessly behind the scenes, without your application ever needing to handle or store a secret.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fZEx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fZEx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png 424w, https://substackcdn.com/image/fetch/$s_!fZEx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png 848w, https://substackcdn.com/image/fetch/$s_!fZEx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png 1272w, https://substackcdn.com/image/fetch/$s_!fZEx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fZEx!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png" width="1200" height="353.57142857142856" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:429,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:305663,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://apurvghai.substack.com/i/177192491?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fZEx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png 424w, https://substackcdn.com/image/fetch/$s_!fZEx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png 848w, https://substackcdn.com/image/fetch/$s_!fZEx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png 1272w, https://substackcdn.com/image/fetch/$s_!fZEx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c92f5ab-9450-40bc-92ec-af2cbfa2a7e8_2649x781.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p><strong>The Code: DefaultAzureCredential to the Rescue</strong></p></blockquote><p>The best part? The Azure SDKs are designed to handle this complexity for you! Your application will automatically detect that it&#8217;s running in an environment with a managed identity and use it to authenticate.</p><p>Here&#8217;s a simple C# code snippet that shows how easy it is to authenticate to Azure Key Vault using <code>IManagedIdentityApplication:</code></p><pre><code><code>    using Microsoft.Identity.Client;
    using System.Threading.Tasks;
    using System;

    // This is the Client ID of the App Registration with the Federated Credential.
    string appRegClientId = &#8220;YOUR_APP_REGISTRATION_CLIENT_ID&#8221;;
    // This is the Tenant ID of your Azure Active Directory.
    string tenantId = &#8220;YOUR_TENANT_ID&#8221;;
    // This is the Client ID of the User-Assigned Managed Identity.
    string managedIdentityClientId = &#8220;YOUR_MANAGED_IDENTITY_CLIENT_ID&#8221;;
    // The scope of the token you want to acquire (e.g., for Microsoft Graph).
    string[] scopes =
    {
        &#8220;https://graph.microsoft.com/.default&#8221; //YOUR_KEY_VAULT_URI
    };
    // 1. Explicitly create the ManagedIdentityId object for the user-assigned identity.
    ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(managedIdentityClientId);
    // 2. Create the IManagedIdentityApplication using the builder and the managedIdentityId object.
    IManagedIdentityApplication miApp = ManagedIdentityApplicationBuilder.Create(managedIdentityId).Build();
    // 3. Define a function to generate the client assertion AND pass within WithClientAssertionMethod as Async Func
    // 4. Use ConfidentialClientApplicationBuilder to create the client with the assertion.
    IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(appRegClientId).WithTenantId(tenantId).WithClientAssertion(async (AssertionRequestOptions options) =&gt;
    {
        AuthenticationResult miResult = await miApp.AcquireTokenForManagedIdentity(&#8221;api://AzureADTokenExchange&#8221;).ExecuteAsync();
        return miResult.AccessToken;
    }).Build();
    try
    {
        // 5. Acquire the token for the desired scope using the confidential client.
        AuthenticationResult finalResult = await app.AcquireTokenForClient(scopes).ExecuteAsync();
        Console.WriteLine($&#8221;Token acquired successfully! Token length: {finalResult.AccessToken.Length}&#8221;);
    }
    catch (MsalException ex)
    {
        Console.WriteLine($&#8221;Error acquiring token: {ex.Message}&#8221;);
    }</code></code></pre><p></p><p><strong>Important:</strong> You need to replace <code>&#8220;YOUR_MANAGED_IDENTITY_CLIENT_ID&#8221;</code> with the Client ID of your User-Assigned Managed Identity and <code>&#8220;YOUR_KEY_VAULT_URI&#8221;</code> with your Key Vault URI.</p><p>Learn more about ClientAssertion Function <a href="https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/web-apps-apis/confidential-client-assertions">here</a></p><blockquote><p><strong>A Final Word</strong></p></blockquote><p>Adopting Federated Identity Credentials with Managed Identities is a significant step towards a more secure and maintenance-free application lifecycle. You can say goodbye to those outages about expired credentials and hello to a more robust and reliable process.</p><p>For more in-depth information, be sure to check out the <a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation">Microsoft Learn documentation on Federated Identity Credentials</a>! &#128218;</p><p>Happy coding! &#10024;</p>]]></content:encoded></item><item><title><![CDATA[Effortless Documentation with DocFX: Build, Deploy, and Scale Without Writing Code ]]></title><description><![CDATA[From Source to Site: Secure, Scalable Docs with DocFX]]></description><link>https://blogs.apurvghai.com/p/effortless-documentation-with-docfx</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/effortless-documentation-with-docfx</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sun, 02 Nov 2025 02:36:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iXc6!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b86d944-be32-485d-9f5b-3b78a43ae97f_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Convert Markdown and YAML into a comprehensive documentation site &#8212; effortless and code-free! Well, maybe just a touch of JSON scripting.</p><p>Have you ever struggled with documentation? What if I told you that you could build a professional-looking docs site without writing a single line of code? Enter <strong><a href="https://dotnet.github.io/docfx/?_blank=">DocFX</a></strong>, a powerful tool that transforms Markdown and YAML into a structured, searchable, and beautifully rendered documentation site.</p><h4><strong>Getting Started</strong></h4><p>Before diving in, let&#8217;s grab our power tools:</p><ul><li><p><strong>PowerShell</strong> for command-line execution</p></li><li><p><strong>DocFX</strong> for site generation</p></li><li><p><strong>Markdown/YAML</strong> for content structuring</p></li><li><p><strong>Azure DevOps or GitHub Repos</strong> (optional) for version control</p></li></ul><h4><strong>Step-by-Step Guide</strong></h4><ul><li><p>Install DocFx using PowerShell, or Dotnet commands.</p></li></ul><pre><code>dotnet tool install docfx --global
docfx init -q</code></pre><ul><li><p><strong>Customize Your Configuration</strong> Modify <code>docfx.json</code> to define content sources, templates, and metadata.</p></li></ul><pre><code>{
    &#8220;build&#8221;: {
        &#8220;content&#8221;: [
            {
                &#8220;files&#8221;: [
                    &#8220;*.{md,yml}&#8221;,
                    &#8220;**/*.{md,yml}&#8221;
                ]
            }
        ],
        &#8220;resource&#8221;: [
            {
                &#8220;files&#8221;: [
                    &#8220;.attachments/**&#8221;,
                    &#8220;images/**.{ico,png}&#8221;,
                    &#8220;*.{ico,png}&#8221;,
                    &#8220;scripts/**.{js}&#8221;,
                    &#8220;web.config&#8221;
                ]
            }
        ],
        &#8220;globalMetadata&#8221;: {
            &#8220;_appTitle&#8221;: &#8220;apurvghai.com&#8221;,
            &#8220;_appLogoPath&#8221;: &#8220;images/logo.png&#8221;,
            &#8220;_appFaviconPath&#8221;: &#8220;favicon.ico&#8221;,
            &#8220;_enableSearch&#8221;: true,
            &#8220;_enableNewTab&#8221;: true,
            &#8220;_gitContribute&#8221;: {
                &#8220;repo&#8221;: &#8220;https://github.com/&lt;yourrepoUrl&gt;&#8221;,
                &#8220;branch&#8221;: &#8220;master&#8221;
            },
            &#8220;_appFooter&#8221;: &#8220;Built using DocFx&#8221;
        },
        &#8220;template&#8221;: [
            &#8220;default&#8221;,
            &#8220;templates/material&#8221;
        ],
        &#8220;postProcessors&#8221;: [
            &#8220;ExtractSearchIndex&#8221;
        ],
        &#8220;force&#8221;: true,
        &#8220;dest&#8221;: &#8220;docs&#8221;,
        &#8220;globalMetadataFiles&#8221;: [],
        &#8220;fileMetadataFiles&#8221;: [],
        &#8220;markdownEngineName&#8221;: &#8220;markdig&#8221;,
        &#8220;noLangKeyword&#8221;: false
    }
}</code></pre><ul><li><p><strong>Organize Your Files</strong> Structure your folders to align with your documentation needs.</p></li></ul><pre><code>- firstDocFxSite
- images
- src
 - AnotherFolder
   - Sub-Article-1.md
   - Article-1.md
   - Getting-Started.md
 - docfx.json
 - index.md
 - toc.yml</code></pre><ul><li><p><strong>Create a Table of Contents (TOC)</strong> Define navigation using <code>toc.yml</code> for seamless browsing.</p></li><li><p>Add following content into your <code>root\toc.yml</code></p></li></ul><pre><code>- name: apurvghai.com 
  href: something.md</code></pre><ul><li><p>Add following content into your <code>root\src\toc.yml</code></p></li></ul><pre><code>    - name: Intro 
      href: ../index.md 
    - name: Getting Started 
      href: Getting-Started.md 
    - name: Article 1 
      href: Article-1.md 
      items: 
        - name: Sub-Article-1.md 
          href: AnotherFolder/Sub-Article-1.md </code></pre><ul><li><p><strong>Build &amp; Preview Locally</strong> Run:</p></li></ul><pre><code>docfx docfx.json --serve --force</code></pre><h4><strong>Build using Azure DevOps Pipelines</strong></h4><p>For advanced users looking to automate website deployment to Azure, configuring a release pipeline with <strong>YAML</strong> offers a streamlined approach.</p><p>By leveraging <strong>Azure DevOps Pipelines</strong>, you can set up a fully automated workflow that builds, validates, and deploys your DocFX-generated site effortlessly.</p><p>Below is a YAML pipeline snippet that can be used to build the site, particularly useful for a Pull Request build.</p><pre><code>- task: AzureCLI@2
  inputs:
    scriptType: &#8216;pscore&#8217;
    scriptLocation: &#8216;inlineScript&#8217;
    inlineScript: |
      # Install the tool
      dotnet tool install docfx --global
      
      docfx docfx.json --build --warningAsErrors true</code></pre><p>Let your documentation speak for itself&#8212;securely, automatically, and effortlessly.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv&#8217;s Sandbox! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Inside the Power BI API: Experiments in Automation and Access]]></title><description><![CDATA[A step-by-step guide to authenticating, accessing, and automating Power BI with C#, Postman, and PowerShell.]]></description><link>https://blogs.apurvghai.com/p/inside-the-power-bi-api-experiments</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/inside-the-power-bi-api-experiments</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sun, 02 Nov 2025 02:34:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iXc6!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b86d944-be32-485d-9f5b-3b78a43ae97f_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello everyone!</p><p>In this article, we&#8217;ll explore how to integrate with the Power BI REST API, focusing on authentication and programmatic access. Whether you&#8217;re a developer, data analyst, or IT professional, this guide will help you take the right steps to connect securely to the Power BI service. The concepts here are language-agnostic, so you can adapt them to your preferred programming environment.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv&#8217;s Sandbox! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h4><strong>Prerequisites</strong></h4><p>Before you begin, ensure you have the following:</p><ul><li><p>Postman (for testing API requests)</p></li><li><p>Basic understanding of OAuth2</p></li><li><p>Access to the Azure Portal</p></li></ul><div><hr></div><h4><strong>Step 1: Register an Application in Azure and Assign Permissions</strong></h4><p>To interact with the Power BI API, you must first register an application in Azure Active Directory and grant it the necessary permissions.</p><ul><li><p><strong>Register your application:</strong><br>Follow the <a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app">official Microsoft guide</a> to register your app.</p></li><li><p><strong>Assign API permissions:</strong><br>The permissions you need depend on the API endpoints you intend to use. For example, to query workspaces, assign either <code>Workspace.ReadWrite.All</code> or <code>Workspace.Read.All</code>.</p><p>You can find a <a href="https://docs.microsoft.com/en-us/rest/api/power-bi/">complete list of Power BI API permissions here</a>.</p></li></ul><blockquote><p><strong>Note:</strong><br>Most Power BI API permissions require delegated access.</p></blockquote><div><hr></div><h4><strong>Step 2: Obtain an Access Token Using Postman</strong></h4><p>You can authenticate using either the Authorization Code flow or Client Credentials. For demonstration, we&#8217;ll use Postman with the Resource Owner Password Credentials (ROPC) grant type.</p><p><strong>Request:</strong></p><pre><code><code>POST https://login.microsoftonline.com/&lt;your-tenant-id&gt;/oauth2/token
Body (x-www-form-urlencoded):
    grant_type: password
    username: &lt;your-username&gt;
    password: &lt;your-password&gt;
    client_id: &lt;your-application-id&gt;
    client_secret: &lt;your-client-secret&gt;
    resource: https://analysis.windows.net/powerbi/api
    scope: Workspace.Read.All User.Read
</code></code></pre><p>Once you send the request, you&#8217;ll receive an access token.</p><blockquote><p><strong>Tip:</strong><br>If you use ADAL libraries, the <code>grant_type=password</code> flow is not supported. Instead, use the User Credential class.</p></blockquote><p>Use the access token to make authorized API calls.</p><p><strong>Example API Call:</strong></p><pre><code><code>GET https://api.powerbi.com/v1.0/myorg/admin/groups?$top=1
Headers:
    Authorization: Bearer &lt;your-access-token&gt;
</code></code></pre><p><strong>Sample Response:</strong></p><pre><code><code>{
  &#8220;@odata.context&#8221;: &#8220;http://wabi-us-central-a-primary-redirect.analysis.windows.net/v1.0/myorg/admin/$metadata#groups&#8221;,
  &#8220;@odata.count&#8221;: 2,
  &#8220;value&#8221;: [
    {
      &#8220;id&#8221;: &#8220;F769F2BB-9825-4B11-8705-998FAF1F8B22&#8221;,
      &#8220;isReadOnly&#8221;: false,
      &#8220;isOnDedicatedCapacity&#8221;: false,
      &#8220;capacityMigrationStatus&#8221;: &#8220;&#8221;,
      &#8220;type&#8221;: &#8220;Workspace&#8221;,
      &#8220;state&#8221;: &#8220;Active&#8221;,
      &#8220;name&#8221;: &#8220;Sample Workspace&#8221;
    }
  ]
}
</code></code></pre><div><hr></div><h4><strong>Step 3: Authenticate Using PowerShell (Non-Interactive / Service Principal)</strong></h4><p>For server-to-server scenarios, use an Azure Service Principal for non-interactive authentication.</p><h4><strong>Prerequisites</strong></h4><ul><li><p><strong>Create a client secret</strong> for your Azure application.<br><a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal">How to create a service principal</a></p></li><li><p><strong>Enable service principal access in Power BI:</strong></p><ul><li><p>Go to the Power BI Admin Portal.</p></li><li><p>Navigate to Tenant Settings.</p></li><li><p>Enable &#8220;Allow service principal to use Power BI APIs&#8221;.</p></li><li><p>Add your app to the required security group.</p></li></ul><p><a href="https://powerbi.microsoft.com/en-us/blog/use-power-bi-api-with-service-principal-preview/">Read more about using service principals with Power BI</a></p></li><li><p><strong>Authorize your app to access workspaces:</strong><br>Use the Power BI PowerShell module to assign roles.</p></li></ul><h4><em>Example PowerShell Script</em></h4><pre><code><code># Install Power BI Module (if not already installed)
Install-Module MicrosoftPowerBIMgmt.Workspaces

# Login interactively
Login-PowerBI

# Service Principal Object ID
$SPObjectId = &#8216;36......&#8217;

# Get the workspace
$pbiWorkspace = Get-PowerBIWorkspace -Name &#8220;&lt;Name of workspace&gt;&#8221;

# Confirm workspace details
Write-Host $pbiWorkspace

# Add Service Principal as Admin or Member
Add-PowerBIWorkspaceUser -Id $pbiWorkspace.Id -AccessRight Admin -PrincipalType App -Identifier $SPObjectId
</code></code></pre><div><hr></div><h4><strong>Step 4: Test Your Integration</strong></h4><p>You can now test your setup using the following PowerShell script, which demonstrates how to obtain a token and trigger a dataset refresh:</p><pre><code><code># Login to Azure CLI
az login

# Retrieve secret from Azure Key Vault
$secretData = az keyvault secret show --vault-name &#8216;&lt;your-keyvault-name&gt;&#8217; --name &#8216;&lt;your-secret-name&gt;&#8217; | ConvertFrom-Json | Select value

$ClientId = &#8220;&lt;Your Client ID/ApplicationId&gt;&#8221;
$ClientSecret = $secretData.value
$loginURL = &#8220;https://login.microsoftonline.com&#8221;
$tenantdomain = &#8220;&lt;your-tenant-domain&gt;&#8221;
$scope1 = &#8220;https://analysis.windows.net/powerbi/api/.default&#8221;
$clientSecret = (ConvertTo-SecureString $ClientSecret -AsPlainText -Force)
$msal = Get-MsalToken -clientID $ClientId -clientSecret $clientSecret -tenantID $tenantdomain -Scopes $scope1
$accessToken = $msal.AccessToken

# Prepare headers
$AuthHeader = @{
  &#8216;Content-Type&#8217;  = &#8216;application/json&#8217;
  &#8216;Authorization&#8217; = &#8220;Bearer $($accessToken)&#8221;
}

# Set Power BI API URL
$URI = &#8220;https://api.powerbi.com/v1.0/myorg/groups/&lt;YourGroupId&gt;/datasets/&lt;YourDataSetId&gt;/refreshes&#8221;

# Trigger dataset refresh
Invoke-RestMethod -Uri $URI -Headers $AuthHeader -Method POST
</code></code></pre><div><hr></div><h4><strong>Conclusion</strong></h4><p>With these steps, you should be able to authenticate and interact with the Power BI REST API programmatically, whether using Postman, PowerShell, or your preferred programming language. If you have any questions or want to share your experience, feel free to leave a comment!</p><p>Happy coding!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv&#8217;s Sandbox! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Building, Breaking, and Blogging: My Tech Journey Continues]]></title><description><![CDATA[A personal shift toward deeper storytelling, publishing, and a renewed passion for experimentation]]></description><link>https://blogs.apurvghai.com/p/building-breaking-and-blogging-my</link><guid isPermaLink="false">https://blogs.apurvghai.com/p/building-breaking-and-blogging-my</guid><dc:creator><![CDATA[Apurv Ghai]]></dc:creator><pubDate>Sat, 01 Nov 2025 22:39:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iXc6!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b86d944-be32-485d-9f5b-3b78a43ae97f_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If there&#8217;s one constant in my tech journey, it&#8217;s my passion for learning. From the very first moment I discovered GitHub Pages, I knew I had to play and experiment with it. The idea of using Liquid, understanding how Ruby powered the infrastructure at the time &#8212; trust me, I dove deep. Setting it all up locally was a challenge, but the thrill of experimentation made it worthwhile. Fun times, indeed!</p><blockquote><p><strong>The Shift to Substack</strong></p></blockquote><p>Today marks an exciting new chapter in my blogging journey. I&#8217;m officially moving to substack, complete with a custom domain and all the bells and whistles that come with it.</p><p><strong>The Blog Archive: A Treasure Trove of Knowledge</strong></p><p>I&#8217;m not porting most my posts over, except few. Most of my previous work lived on MSDN, and when Microsoft transitioned those to <a href="https://learn.microsoft.com/en-us/archive/blogs/apurvghai/">Microsoft Learn</a>, they found a new home here. While Dynamics has evolved quite a bit since then, there are still some gems in that archive that remain relevant today. If you&#8217;re passionate about CRM development, OAuth authentication, Web API integrations, and more, I hope you find the archive useful.</p><p><strong>What&#8217;s Next?</strong></p><p>This shift doesn&#8217;t mean I&#8217;m slowing down. Quite the opposite &#8212; I&#8217;m excited to bring fresh insights, new experiments, and deeper technical explorations to Substack. If you&#8217;ve followed my journey so far, I promise to keep delivering content that keeps you engaged, learning, and experimenting right alongside me.</p><p>To all my fellow technical programmers out there &#8212; here&#8217;s to the next phase. Let&#8217;s build, break, and learn together. &#128640;</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blogs.apurvghai.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Apurv's Blog! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item></channel></rss>