<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-04-14T13:21:41+02:00</updated><id>/feed.xml</id><title type="html">jjohnsen.no</title><subtitle>Adventures in Azure and Power Platform</subtitle><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><entry><title type="html">Proxmox Network Isolation - Isolate LXC Container from Local Network</title><link href="/2026/proxmox-network-isolation/" rel="alternate" type="text/html" title="Proxmox Network Isolation - Isolate LXC Container from Local Network" /><published>2026-04-14T00:00:00+02:00</published><updated>2026-04-14T00:00:00+02:00</updated><id>/2026/proxmox-network-isolation</id><content type="html" xml:base="/2026/proxmox-network-isolation/"><![CDATA[<p>When you run a service like WordPress in your homelab and expose it to the internet (via Cloudflare Tunnel, reverse proxy, etc.), you’re creating an entry point that the outside world can reach. By default, this container sits on your home network alongside other devices, and if it’s compromised, an attacker could pivot to attack your NAS, Proxmox host, or other devices on the LAN.</p>

<p>The practical question is: does your WordPress container actually <em>need</em> access to your internal network? The answer is almost always no. WordPress needs internet access to fetch updates, plugins, and reach Cloudflare. But it has no legitimate reason to reach other devices on your LAN.</p>

<p>Network isolation enforces this principle: the container gets full internet access via NAT, but is explicitly denied access to your local network. If WordPress is compromised by a vulnerability or malicious plugin, an attacker cannot pivot internally to attack your infrastructure. 🛡️</p>

<h2 id="why-network-isolation">Why Network Isolation?</h2>

<ul>
  <li><strong>Security by default</strong> — limit the blast radius if a container is compromised</li>
  <li><strong>Network segmentation</strong> — different containers have different trust levels and access requirements</li>
  <li><strong>Compliance-like boundaries</strong> — enforce what each service should reach</li>
  <li><strong>Simplified troubleshooting</strong> — catch unintended network access patterns</li>
</ul>

<h2 id="what-youll-achieve">What You’ll Achieve</h2>

<ul>
  <li>✅ Container gets internet access via NAT</li>
  <li>✅ Container cannot reach your local LAN</li>
  <li>✅ DNS works via public resolvers or Tailscale</li>
  <li>✅ WordPress hosted on the container is accessible through Cloudflare Tunnel</li>
</ul>

<h2 id="step-by-step-setup">Step-by-Step Setup</h2>

<h3 id="step-1-create-an-isolated-bridge-on-proxmox">Step 1: Create an Isolated Bridge on Proxmox</h3>

<p>SSH into your Proxmox host and edit the network config:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano /etc/network/interfaces
</code></pre></div></div>

<p>Add a new bridge with no physical uplink (this is the isolation magic):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>auto vmbr1
iface vmbr1 inet static
        address 10.10.10.1/24
        bridge-ports none
        bridge-stp off
        bridge-fd 0
        post-up iptables -t nat -A POSTROUTING -s '10.10.10.0/24' -o vmbr0 -j MASQUERADE
        post-up iptables -I FORWARD 1 -i vmbr1 -d 192.168.1.0/24 -j REJECT
        post-down iptables -t nat -D POSTROUTING -s '10.10.10.0/24' -o vmbr0 -j MASQUERADE
        post-down iptables -D FORWARD -i vmbr1 -d 192.168.1.0/24 -j REJECT        
</code></pre></div></div>

<p>What each line does:</p>

<table>
  <thead>
    <tr>
      <th>Line</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">address 10.10.10.1/24</code></td>
      <td>Gateway IP for the isolated subnet</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bridge-ports none</code></td>
      <td>No physical NIC attached — this prevents LAN traffic</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">post-up iptables -t nat...MASQUERADE</code></td>
      <td>Enable NAT so container traffic leaves the Proxmox host</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">post-up iptables -I FORWARD...-d 192.168.1.0/24</code></td>
      <td>Block forwarding to your LAN</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">post-down iptables -t nat...-D</code></td>
      <td>Clean up the NAT rule when bridge stops</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">post-down iptables -D FORWARD</code></td>
      <td>Clean up the block rule</td>
    </tr>
  </tbody>
</table>

<p>Enable IPv4 forwarding (required for NAT):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"net.ipv4.ip_forward=1"</span> <span class="o">&gt;&gt;</span> /etc/sysctl.conf
sysctl <span class="nt">-p</span> <span class="c"># Apply immediately</span>
</code></pre></div></div>

<h3 id="step-2-restart-networking">Step 2: Restart Networking</h3>

<p>Bring up the new bridge:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ifdown vmbr1 <span class="o">&amp;&amp;</span> ifup vmbr1
</code></pre></div></div>

<p>Verify it exists:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip addr show vmbr1
brctl show vmbr1
</code></pre></div></div>

<p>Verify the NAT and FORWARD rules were added:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iptables <span class="nt">-t</span> nat <span class="nt">-L</span> POSTROUTING <span class="nt">-n</span> <span class="nt">-v</span>
iptables <span class="nt">-L</span> FORWARD <span class="nt">-n</span> <span class="nt">-v</span>
</code></pre></div></div>

<h3 id="step-3-attach-container-to-the-isolated-bridge">Step 3: Attach Container to the Isolated Bridge</h3>

<p>In the Proxmox web UI:</p>

<ol>
  <li>Open your container</li>
  <li>Go to <strong>Network</strong></li>
  <li>Open the <strong>Network Device</strong></li>
  <li>Change <strong>Bridge</strong> from <code class="language-plaintext highlighter-rouge">vmbr0</code> to <code class="language-plaintext highlighter-rouge">vmbr1</code></li>
  <li>Set the IP Configuration to Static and use an IP in the subnet</li>
  <li>Go to <strong>DNS</strong></li>
  <li>Set the DNS Servers to <code class="language-plaintext highlighter-rouge">1.1.1.1 1.0.0.1</code> (Cloudflare DNS) or your preferred public DNS</li>
  <li>Reboot the container to apply all changes</li>
</ol>

<p><img src="proxmox-lxc-network-device-vmbr1-static-ip.png" alt="Proxmox LXC network device settings using vmbr1 with static IP 10.10.10.2 and gateway 10.10.10.1" />
<img src="proxmox-lxc-dns-servers-cloudflare.png" alt="Proxmox LXC DNS settings configured with Cloudflare DNS servers 1.1.1.1 and 1.0.0.1" /></p>

<h2 id="verification">Verification</h2>

<p>Inside the container, run these tests:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Test 1: Can reach the gateway (required for any external traffic)</span>
ping <span class="nt">-c</span> 3 10.10.10.1

<span class="c"># Test 2: Can reach the internet (NAT working)</span>
ping <span class="nt">-c</span> 3 1.1.1.1

<span class="c"># Test 3: Cannot reach local network (isolation working)</span>
<span class="c"># Use an actual IP from your LAN, e.g., your NAS or router</span>
ping <span class="nt">-c</span> 3 192.168.1.15

<span class="c"># Test 4: DNS works</span>
nslookup vg.no
</code></pre></div></div>

<p>Expected results:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ ping 10.10.10.1 — success (2 packets → 2 received)
✅ ping 1.1.1.1 — success (3 packets → 3 received)
❌ ping 192.168.1.15 — fails (Destination Host Unreachable or Port Unreachable)
✅ nslookup vg.no — resolves and returns an IP
</code></pre></div></div>

<h2 id="debugging-network-issues">Debugging Network Issues</h2>

<p>If internet fails, verify on the Proxmox host:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check IP forwarding is enabled</span>
sysctl net.ipv4.ip_forward

<span class="c"># Check NAT rule exists</span>
iptables <span class="nt">-t</span> nat <span class="nt">-L</span> POSTROUTING <span class="nt">-n</span> <span class="nt">-v</span>

<span class="c"># Check bridge membership</span>
brctl show vmbr1

<span class="c"># Test connectivity from host</span>
ping <span class="nt">-c</span> 1 10.10.10.2
</code></pre></div></div>

<h2 id="ending">Ending</h2>

<p>This setup gives you a practical security boundary for internet-facing homelab services: keep outbound internet access for updates and integrations, while blocking lateral movement into your LAN.</p>

<p>For me, this is now a baseline pattern in Proxmox: if a service is exposed publicly, it should live on an isolated bridge with explicit egress rules instead of full LAN trust.</p>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="Proxmox" /><category term="Homelab" /><category term="Network" /><category term="LXC" /><category term="Security" /><summary type="html"><![CDATA[Set up a Proxmox LXC container with isolated network access - allow outbound internet traffic while blocking access to the local network.]]></summary></entry><entry><title type="html">ESPHome Altherma - Monitor your Daikin Altherma 3 heat pump via X10A</title><link href="/2026/esphome-altherma-monitor-daikin-heat-pump/" rel="alternate" type="text/html" title="ESPHome Altherma - Monitor your Daikin Altherma 3 heat pump via X10A" /><published>2026-01-05T00:00:00+01:00</published><updated>2026-01-05T00:00:00+01:00</updated><id>/2026/esphome-altherma-monitor-daikin-heat-pump</id><content type="html" xml:base="/2026/esphome-altherma-monitor-daikin-heat-pump/"><![CDATA[<p>I’ve been running a custom ESPHome implementation for my Daikin Altherma heat pump since 2022, and I finally got around to doing a proper rewrite. The result is <a href="https://github.com/jjohnsen/esphome-altherma">ESPHome Altherma</a> - a native ESPHome custom component that monitors your Daikin Altherma 3 heat pump via the X10A connector and integrates directly with Home Assistant.</p>

<p>Inspired by <a href="https://github.com/raomin/ESPAltherma">ESPAltherma</a>, but built as a proper ESPHome component - so you get OTA updates, auto-discovery, and all the other ESPHome goodness out of the box.</p>

<p><img src="esphome-dashboard.png" alt="Home Assistant dashboard showing Altherma sensor data" /></p>

<p><img src="esphome-device-entities.png" alt="ESPHome device page with Altherma entities" /></p>

<h2 id="features">Features</h2>

<ul>
  <li><strong>Browser-based installation</strong> - flash directly from your browser via <a href="https://esphome.github.io/esp-web-tools/">ESP Web Tools</a>, no coding required</li>
  <li><strong>Temperature monitoring</strong> - outdoor air, discharge pipe, heat exchanger, leaving water, inlet water, DHW tank, etc.</li>
  <li><strong>Electrical sensors</strong> - inverter current, voltage</li>
  <li><strong>Operational data</strong> - compressor frequency, fan speeds, flow rate, water pressure, pump signal</li>
  <li><strong>Binary sensors</strong> - defrost status, thermostat ON/OFF, silent mode, freeze protection, 3-way valve, BUH steps</li>
  <li><strong>Diagnostics</strong> - operation mode, error codes and sub-codes</li>
  <li><strong>Multiple board support</strong> - ESP32, ESP32-S3, M5Stack AtomS3 Lite</li>
  <li><strong>Model-specific configs</strong> - modular YAML files per heat pump model</li>
  <li><strong>Mock UART mode</strong> - for development/testing without hardware</li>
  <li><strong>OTA updates</strong> - with automatic update checking via GitHub releases</li>
</ul>

<h2 id="hardware">Hardware</h2>

<p>Any ESP32 board with a UART will work. I’ve tested with:</p>

<table>
  <thead>
    <tr>
      <th>Board</th>
      <th>YAML Config</th>
      <th>UART RX Pin</th>
      <th>UART TX Pin</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://www.espboards.dev/esp32/esp32doit-devkit-v1/">ESP32 DevKit</a></td>
      <td><code class="language-plaintext highlighter-rouge">esphome-altherma-esp32.yaml</code></td>
      <td>GPIO 16</td>
      <td>GPIO 17</td>
    </tr>
    <tr>
      <td><a href="https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp32-s3-devkitc-1/">ESP32-S3 DevKit</a></td>
      <td><code class="language-plaintext highlighter-rouge">esphome-altherma-esp32-s3.yaml</code></td>
      <td>GPIO 2</td>
      <td>GPIO 1</td>
    </tr>
    <tr>
      <td><a href="https://docs.m5stack.com/en/core/AtomS3%20Lite">M5Stack AtomS3 Lite</a></td>
      <td><code class="language-plaintext highlighter-rouge">esphome-altherma-atoms3.yaml</code></td>
      <td>GPIO 2 (G1)</td>
      <td>GPIO 1 (G2)</td>
    </tr>
  </tbody>
</table>

<h2 id="further-reading">Further Reading</h2>

<ul>
  <li><strong>GitHub:</strong> <a href="https://github.com/jjohnsen/esphome-altherma">github.com/jjohnsen/esphome-altherma</a></li>
  <li><strong>Web installer:</strong> <a href="https://jjohnsen.github.io/esphome-altherma/">jjohnsen.github.io/esphome-altherma</a></li>
</ul>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="ESPHome" /><category term="Home Assistant" /><category term="Daikin" /><category term="Heat Pump" /><category term="ESP32" /><category term="IoT" /><summary type="html"><![CDATA[A native ESPHome custom component for monitoring Daikin Altherma 3 heat pumps via the X10A connector - with browser-based installation and Home Assistant auto-discovery.]]></summary></entry><entry><title type="html">Migrating Azure DevOps Projects - Part 2: Boards &amp;amp; Work Items</title><link href="/2025/migrating-azure-devops-projects-part-2-boards-and-work-items/" rel="alternate" type="text/html" title="Migrating Azure DevOps Projects - Part 2: Boards &amp;amp; Work Items" /><published>2025-02-10T00:00:00+01:00</published><updated>2025-02-10T00:00:00+01:00</updated><id>/2025/migrating-azure-devops-projects-part-2-boards-and-work-items</id><content type="html" xml:base="/2025/migrating-azure-devops-projects-part-2-boards-and-work-items/"><![CDATA[<p><img src="migrating-azure-devops-header.png" alt="Migrating Azure DevOps Part 2 - illustration showing Azure Boards, Azure Repos, Azure Pipelines, Azure Test Plans, and Azure Artifacts with Azure Boards highlighted" /></p>

<p>In <a href="/2025/migrating-azure-devops-projects-part-1-repos-and-wikis/">Part 1</a>, we covered how to migrate Repos and Wikis. Now we’ll look into the more complex challenge of migrating Boards and Work Items.</p>

<p>This process is more demanding and requires additional tooling. Fortunately, <a href="https://github.com/nkdAgility/azure-devops-migration-tools">azure-devops-migration-tools</a>, created by Martin Hinshelwood, provides a way to move Work Items, Test Plans &amp; Suites, Pipelines and more between organizations.</p>

<p>The learning curve can, however, be steep—but don’t worry! We’ll break it down step by step and walk through the entire process in this part. 💪</p>

<h2 id="installation">💾Installation</h2>

<p>Before we begin, azure-devops-migration-tools must be installed. Since it’s a Windows application, it can be installed using winget:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>winget install nkdagility.azure-devops-migration-tools
</code></pre></div></div>

<p>For other options check the <a href="https://nkdagility.com/learn/azure-devops-migration-tools/setup/installation/">installation documentation</a>.</p>

<h3 id="️creating-a-config-file">✍️Creating a Config File</h3>

<p>The tool runs from the command line and requires a JSON configuration file to define how the migration should be performed. A basic starting point for the config file looks as follows:</p>

<p>example.conf.json:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Serilog"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"MinimumLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Information"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"MigrationTools"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"16.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Endpoints"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"Source"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"EndpointType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TfsTeamProjectEndpoint"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"Collection"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://dev.azure.com/old-org/"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"Project"</span><span class="p">:</span><span class="w"> </span><span class="s2">"old-project"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"Authentication"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"AuthenticationMode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AccessToken"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"AccessToken"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[Personal Access Token Old Org]"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"Target"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"EndpointType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TfsTeamProjectEndpoint"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"Collection"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://dev.azure.com/new-org/"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"Project"</span><span class="p">:</span><span class="w"> </span><span class="s2">"new-project"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"Authentication"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"AuthenticationMode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AccessToken"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"AccessToken"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[Personal Access Token New Org]"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"ReflectedWorkItemIdField"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Custom.ReflectedWorkItemId"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"CommonTools"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"TfsUserMappingTool"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"Enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
        </span><span class="nl">"IdentityFieldsToCheck"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="s2">"System.AssignedTo"</span><span class="p">,</span><span class="w">
          </span><span class="s2">"System.ChangedBy"</span><span class="p">,</span><span class="w">
          </span><span class="s2">"System.CreatedBy"</span><span class="w">
        </span><span class="p">]</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"Processors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="nl">"ProcessorType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TfsWorkItemMigrationProcessor"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"Enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
        </span><span class="nl">"WIQLQuery"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan','Shared Steps','Shared Parameter','Feedback Request') ORDER BY [System.ChangedDate] desc"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>To use this config, you’ll need to update the following fields:</p>

<ul>
  <li><strong>Collection</strong>: The URL of your Azure DevOps source and target organizations.</li>
  <li><strong>Project</strong>: The names of the source and target projects.</li>
</ul>

<p>💡 If the project name contains spaces, ensure the Project field reflects that exactly (don’t use HTML-encoded %20 for spaces).</p>

<ul>
  <li><strong>AccessToken</strong>: Personal Access Token (PAT) for both the source and target organizations.</li>
</ul>

<h2 id="personal-access-tokens-pat">🔐Personal Access Tokens (PAT)</h2>

<p>The migration tool requires full access PAT tokens for both the source and target organizations. These can be created from User settings -&gt; Personal Access Tokens:</p>

<p><img src="pat-settings.png" alt="Personal Access Token settings in Azure DevOps" /></p>

<p>If your DevOps policy restricts full access, I’ve still had success by setting each individual scope to the highest level available.</p>

<h2 id="️target-project-and-reflectedworkitemid-process-field">⚙️Target Project and ReflectedWorkItemId Process Field</h2>

<p>The migration tool uses a special field, ReflectedWorkItemId, to track the migration of work items. This field must be created on all work items that will be migrated.</p>

<p>What I usually do is create a new inherited process in the target organization that matches the source project’s process. This ensures that the ReflectedWorkItemId field is added to all work items in the target project.</p>

<p>Subsequent migrations can, of course, use the same process.</p>

<p><img src="inherited-process.png" alt="Creating a new inherited process in Azure DevOps" /></p>

<p><img src="add-field-first.png" alt="Adding the ReflectedWorkItemId Field to the First Work Item Type (Bug)" /></p>

<p><em>Adding the ReflectedWorkItemId Field to the First Work Item Type (Bug)</em></p>

<p><img src="add-field-existing.png" alt="Adding the ReflectedWorkItemId Field to the Next Work Item Type when the field already exists" /></p>

<p><em>Adding the ReflectedWorkItemId Field to the Next Work Item Type when the field already exists</em></p>

<p>Once the ReflectedWorkItemId field has been added to all work item types, you can create the target project using the inherited process.</p>

<p><img src="create-project.png" alt="Creating a project with the new inherited process" /></p>

<p><em>Creating a project with the new inherited process</em></p>

<p>💡Microsoft offers a separate tool for exporting and importing inherited processes. If you’ve made extensive process customizations, it’s worth checking out. You can find the tool here: <a href="https://github.com/microsoft/process-migrator">VSTS Process Migrator</a>.</p>

<h2 id="board-settings">📋Board Settings</h2>

<p>If you’ve customized Board Settings (such as columns or swimlanes), make sure to replicate these settings in the target project before executing the migration. This ensures that the work items are placed correctly on the target board.</p>

<p><img src="board-settings.png" alt="Make sure to replicate Board settings before migration" />
<em>Make sure to replicate Board settings before migration</em></p>

<h2 id="executing-the-migrating-processes">🚀Executing the Migrating Processes</h2>

<p>Given that the Source and Target configuration is correct you should now be able to start a migration with the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>devopsmigration execute --config .\example.conf.json
</code></pre></div></div>

<p>If all goes well, your work items and iterations should now be migrated successfully. Don’t forget to validate the migrated data and make any adjustments as necessary.</p>

<p><img src="migration-output.png" alt="Migration execution output showing successful work item migration" /></p>

<hr />

<p>That concludes Part 2 of this series! ✅ Stay tuned for Part 3, where we’ll dive deeper into migrating Azure DevOps Test Plans, Pipelines, and more. 🚀</p>

<hr />

<h3 id="-azure-devops-migration-series-">🔹 Azure DevOps Migration Series 🔹</h3>

<p><a href="/2025/migrating-azure-devops-projects-part-1-repos-and-wikis/">🚀 Part 1: Migrating Repos &amp; Wikis</a> | <a href="https://www.linkedin.com/pulse/migrating-azure-devops-projects-part-1-jan-%C3%A5ge-johnsen-unqzf/">🔗LinkedIn</a></p>

<p>📊 Part 2: Migrating Boards &amp; Work Items | <a href="https://www.linkedin.com/pulse/migrating-azure-devops-projects-part-2-boards-work-items-johnsen-igzvf/">🔗LinkedIn</a></p>

<p>🧪 Part 3: (Coming soon!)</p>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="Azure DevOps" /><category term="Migration" /><category term="Boards" /><category term="Work Items" /><summary type="html"><![CDATA[Migrating Azure DevOps Boards and Work Items between organizations using azure-devops-migration-tools.]]></summary></entry><entry><title type="html">Migrating Azure DevOps Projects - Part 1: Repos and Wikis</title><link href="/2025/migrating-azure-devops-projects-part-1-repos-and-wikis/" rel="alternate" type="text/html" title="Migrating Azure DevOps Projects - Part 1: Repos and Wikis" /><published>2025-02-03T00:00:00+01:00</published><updated>2025-02-03T00:00:00+01:00</updated><id>/2025/migrating-azure-devops-projects-part-1-repos-and-wikis</id><content type="html" xml:base="/2025/migrating-azure-devops-projects-part-1-repos-and-wikis/"><![CDATA[<p><img src="migrating-azure-devops-header.png" alt="Migrating Azure DevOps - illustration showing Azure Boards, Azure Repos, Azure Pipelines, Azure Test Plans, and Azure Artifacts" /></p>

<p>Migrating Azure DevOps projects between organizations (hopefully) isn’t something you do every day, but when the need arises—whether due to a merger, reorganization, or other reasons—it can be a challenging process.</p>

<p>Breaking the work down, you’re mostly dealing with Boards/Work Items, Repos, Wikis, Pipelines, Test Plans, and Artifacts.</p>

<p>You’d expect Microsoft to provide a seamless way to handle this, but unfortunately, no single tool does it all. Instead, you’ll need a mix of different tools and approaches to get the job done.</p>

<p>So, let’s start with something fairly simple:</p>

<h2 id="-repos">📂 Repos</h2>

<p>Since you’re hopefully using Git by now, migrating Repos is fairly straightforward using the following steps:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Mirror the Existing Repository</span>
git clone <span class="nt">--mirror</span> https://dev.azure.com/old-org/old-project/_git/repo-name

<span class="c"># Push to new Repository (must be created beforehand)</span>
<span class="nb">cd </span>repo-name.git
git push <span class="nt">--mirror</span> https://[username]:[password]@dev.azure.com/new-org/new-project/_git/repo-name
</code></pre></div></div>

<p>Using <code class="language-plaintext highlighter-rouge">--mirror</code> ensures that all branches, tags, and Git refs are cloned and pushed, fully replicating the repository.</p>

<p>If you’re moving between organizations with different credentials, it’s often easiest to use Git Credentials directly in the command line (<code class="language-plaintext highlighter-rouge">[username]:[password]</code>). You can generate these credentials from the Clone Repository panel by clicking <strong>Generate Git Credentials</strong>:</p>

<p><img src="generate-git-credentials.png" alt="Azure DevOps Clone Repository dialog showing HTTPS command line URL and Generate Git Credentials button" /></p>

<blockquote>
  <p>⚠️ <strong>Important:</strong> While links from work items to commits should continue to work, the reverse—links from Git commit logs to work items—won’t. This happens because work items get new IDs during migration. I’ll cover how to handle this later in an advanced part 🤓.</p>
</blockquote>

<h2 id="-disabling-the-repository">❌ Disabling the Repository</h2>

<p>After migrating the repo, it’s a good practice to disable access to the old repository. This will prevent any accidental usage while keeping the repository discoverable with a warning.</p>

<p>This can be done from <strong>Manage Repository -&gt; [Repository] -&gt; Settings -&gt; Disable Repository</strong></p>

<p><img src="disable-repository.png" alt="Azure DevOps disabled repository page showing 'Repository Target project is disabled' message with UFO illustration" /></p>

<h2 id="-wikis">📖 Wikis</h2>

<p>With the repository successfully migrated, Wikis are fairly simple to move since they are also stored as Git repositories. The only difficulty can be locating the repo URL, which can be found under the <strong>Clone wiki</strong> menu item on the Wiki page:</p>

<p><img src="clone-wiki.png" alt="Azure DevOps Wiki page showing the Clone wiki option highlighted in the dropdown menu" /></p>

<blockquote>
  <p>⚠️ <strong>Important:</strong> As with commit messages, links to work items (#12345) won’t work after migration. This happens because work items get new IDs during migration. I’ll cover how to handle this later in an advanced part 🤓.</p>
</blockquote>

<hr />

<p>That concludes Part 1 of this series! ✅ Stay tuned for Part 2, where we’ll dive into migrating Boards and Work Items, and in future parts, we’ll explore migrating Test Plans, Pipelines, and more. 🚀</p>

<hr />

<h3 id="-azure-devops-migration-series-">🔹 Azure DevOps Migration Series 🔹</h3>

<p>🚀 Part 1: Migrating Repos &amp; Wikis | <a href="https://www.linkedin.com/pulse/migrating-azure-devops-projects-part-1-jan-%C3%A5ge-johnsen-unqzf/">🔗LinkedIn</a></p>

<p><a href="/2025/migrating-azure-devops-projects-part-2-boards-and-work-items/">📊 Part 2: Migrating Boards &amp; Work Items</a> | <a href="https://www.linkedin.com/pulse/migrating-azure-devops-projects-part-2-boards-work-items-johnsen-igzvf/">🔗LinkedIn</a></p>

<p>🧪 Part 3: (Coming soon!)</p>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="Azure DevOps" /><category term="Git" /><category term="Migration" /><category term="Wikis" /><summary type="html"><![CDATA[Migrating Azure DevOps projects between organizations—covering Repos and Wikis using Git mirror cloning.]]></summary></entry><entry><title type="html">Getting started with Power Pages + Vue.js</title><link href="/2023/getting-started-with-power-pages-vuejs/" rel="alternate" type="text/html" title="Getting started with Power Pages + Vue.js" /><published>2023-11-15T00:00:00+01:00</published><updated>2023-11-15T00:00:00+01:00</updated><id>/2023/getting-started-with-power-pages-vuejs</id><content type="html" xml:base="/2023/getting-started-with-power-pages-vuejs/"><![CDATA[<h2 id="step-1-creating-the-vuejs-project">Step 1: Creating the Vue.js project</h2>

<h3 id="scaffolding-the-vuejs-project">Scaffolding the Vue.js project</h3>

<p>To quickly setup a project we use the create-vue tool.</p>

<p>Drop down to your favorite shell and run: <code class="language-plaintext highlighter-rouge">npm create vue@latest</code>.</p>

<p>Please note that I’ve added Pinia for future state management.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; npm create vue@latest

Need to install the following packages:
  create-vue@3.8.0
Ok to proceed? (y)

Vue.js - The Progressive JavaScript Framework

√ Project name: ... vue-in-powerpages
√ Add TypeScript? ... No
√ Add JSX Support? ... No
√ Add Vue Router for Single Page Application development? ... No
√ Add Pinia for state management? ... Yes
√ Add Vitest for Unit Testing? ... No
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... No

Scaffolding project in C:\repos\Vuejs-in-PowerPages\vue-in-powerpages...

Done. Now run:

  cd vue-in-powerpages
  npm install
  npm run dev
</code></pre></div></div>

<h3 id="starting-the-developer-server">Starting the developer server</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd vue-in-powerpages
npm install
npm run dev
</code></pre></div></div>

<p>The server should now start, and you can open your browser at <a href="http://localhost:5173/">http://localhost:5173/</a></p>

<p><img src="vue-dev-server.png" alt="Vue.js dev server running in the browser on localhost:5173" /></p>

<p>Pat yourself on the back! You did it! Your first app is up and running!</p>

<h2 id="step-2-adding-the-app-to-power-pages">Step 2: Adding the app to Power Pages</h2>

<p>The next steps requires some more work, both in Power Pages and in the Vue.js app.</p>

<h3 id="power-pages">Power Pages</h3>

<h4 id="add-a-page">Add a page</h4>

<p>We’ll start in Power Pages by creating a new page for the component to live on:</p>

<p><img src="power-pages-new-page.png" alt="Creating a new page in Power Pages" /></p>

<p>Once the page is created we’ll open it in Visual Studio to add some customizations:</p>

<p><img src="power-pages-open-vscode.png" alt="Opening the page in Visual Studio Code" /></p>

<p>Here we’ll add a div for the app and load the script from localhost:</p>

<p><img src="power-pages-add-div.png" alt="Adding the div and script tag for the Vue app" /></p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"app"</span><span class="nt">&gt;</span>The Vue app will live here!<span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"module"</span> <span class="na">src=</span><span class="s">"https://localhost:5173/src/main.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>Save the file, head back to Portals Studio and remember to Sync before previewing the page.</p>

<p><img src="power-pages-sync.png" alt="Sync button in Power Pages" /></p>

<p>You should now see the placeholder div on the page, but unfortunately it wont load before we make some changes to the Vue app.</p>

<p><img src="power-pages-placeholder-div.png" alt="Placeholder div visible on the Power Pages page" /></p>

<p><img src="power-pages-console-errors.png" alt="Browser console showing errors loading the Vue app" /></p>

<h3 id="adapting-the-vue-app-for-power-pages">Adapting the Vue app for Power Pages</h3>

<p>As stated above we need to make some adjustment before the app can be loaded in Power Pages.</p>

<h4 id="turn-on-https">Turn on HTTPS</h4>

<p>Power Pages runs on HTTPS, while the local development server now uses HTTP. To prevent your browser from blocking the app, it’s necessary to enable HTTPS on the local server.</p>

<p>To enable https we first need to install the <a href="https://github.com/vitejs/vite-plugin-basic-ssl">vite plugin basic ssl</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; cd vue-in-powerpages
&gt; npm install --save-dev @vitejs/plugin-basic-ssl
</code></pre></div></div>

<p>We then need to add it to the vite.config.js:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">fileURLToPath</span><span class="p">,</span> <span class="nx">URL</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:url</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">basicSsl</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@vitejs/plugin-basic-ssl</span><span class="dl">'</span> <span class="c1">// Load basic SSL plugin</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vite</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">vue</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@vitejs/plugin-vue</span><span class="dl">'</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="na">server</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">https</span><span class="p">:</span> <span class="kc">true</span> <span class="c1">// Enable HTTPS for the server</span>
  <span class="p">},</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span>
    <span class="nx">vue</span><span class="p">(),</span>
    <span class="nx">basicSsl</span><span class="p">()</span> <span class="c1">// Use basic SSL plugin for HTTPS</span>
  <span class="p">],</span>
  <span class="na">resolve</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">alias</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">'</span><span class="s1">@</span><span class="dl">'</span><span class="p">:</span> <span class="nx">fileURLToPath</span><span class="p">(</span><span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="dl">'</span><span class="s1">./src</span><span class="dl">'</span><span class="p">,</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<h3 id="opening-the-vue-app-in-power-pages">Opening the Vue app in Power Pages</h3>

<p>Starting the dev server - it should now be listening on HTTPS:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; npm run dev
VITE v4.5.0  ready in 701 ms

  ➜  Local:   https://localhost:5173/
</code></pre></div></div>

<p>Opening <a href="https://localhost:5173/">https://localhost:5173/</a> will now yield a warning that you need to accept:</p>

<p><img src="https-cert-warning.png" alt="Browser HTTPS certificate warning" /></p>

<p><img src="https-cert-accept.png" alt="Accepting the self-signed certificate" /></p>

<p>You should now be able reload the page we created in Power Pages and load the app.</p>

<p>And as if that wasn’t enough you should now be able to do live editing of the Vue app and instantly see the changes on save!</p>

<p><img src="vue-and-powerpages.gif" alt="Vue.js app running inside Power Pages with live reload" /></p>

<p>Once again, pat your self on the back! Well done!</p>

<hr />

<p><em>Source: <a href="https://github.com/jjohnsen/vue-in-powerpages">GitHub - jjohnsen/vue-in-powerpages</a></em></p>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="Power Pages" /><category term="Vue.js" /><category term="JavaScript" /><category term="Vite" /><summary type="html"><![CDATA[A step-by-step guide to setting up a Vue.js app and integrating it with Microsoft Power Pages using Vite's dev server and HTTPS.]]></summary></entry><entry><title type="html">Get started with Azure API Management and Dynamics 365 in 30 minutes</title><link href="/2022/apim-and-crm-in-30-minutes/" rel="alternate" type="text/html" title="Get started with Azure API Management and Dynamics 365 in 30 minutes" /><published>2022-06-02T00:00:00+02:00</published><updated>2022-06-07T00:00:00+02:00</updated><id>/2022/apim-and-crm-in-30-minutes</id><content type="html" xml:base="/2022/apim-and-crm-in-30-minutes/"><![CDATA[<p>Dynamics 365 comes with a robust and powerful REST API out of the box.
However, when integrating with other parties, I prefer to use Azure API Management (APIM) as an intermediary for a multitude of reasons.</p>

<p>The setup used to be somewhat convoluted, but is now  much simpler.
This post will walk you through the steps needed for setting it up manually, while a later post will detail how to automate it though a DevOps pipeline.</p>

<h2 id="1-create-the-api-management-service">1. Create the API Management service</h2>

<p>The first step is to install the API Management gateway, so head over to the Azure portal and create a new API Management service.</p>

<p>There aren’t many options that needs to be configured, but make sure to enable <em>System assigned managed identity</em> as it will be used to connect to the Dynamics 365 instance.</p>

<p><img src="Install-API-Management-gateway.png" alt="Install API Management gateway - Managed identity" /></p>

<h2 id="2-create-an-app-user">2. Create an app user</h2>

<p>The next step is to create an app user for the managed identity, in the environment it should have access to.</p>

<p>First we need the <strong>Object ID</strong> for the Managed Identity. This can be found on <em>Security -&gt; Managed identities -&gt; System assigned</em>:</p>

<p><img src="mi-object-id.png" alt="Object Id for Managed Identity" /></p>

<p>Then go to <em>Azure Active Directory -&gt; Enterprise Applications</em> and search for this Object ID to find the <strong>Application ID</strong>:
<img src="ea-application-id.png" alt="Application ID for Managed Identity" /></p>

<hr />

<p>Head over to the <a href="https://admin.powerplatform.microsoft.com">Power Platform admin center</a>, open the environment that APIM should get access to, click <em>S2s Apps -&gt; See all</em>:
<img src="s2s-apps.png" alt="Enviroment details" /></p>

<p>Click <em>New app user</em>:
<img src="new-app-user.png" alt="New app user" class="align-center" /></p>

<p>In the <em>Create a new app user</em> dialog, Click <em>Add an app</em> and search for the <strong>Application ID</strong>, select to app and click <em>Add</em>:
<img src="add-app-from-aad.png" alt="Search for Application ID" /></p>

<p>Select a <em>Business unit</em>, add <em>Security roles</em> and click <em>Create</em>:
<img src="create-a-new-app-user.png" alt="Create a new app user" /></p>

<p><em>I recommend creating a separate security role for APIM, but that is outside the scope of this post.</em></p>

<h2 id="3-creating-an-api-and-operation">3. Creating an API and operation</h2>

<p>To test the connection we need to create a new API with an operation that calls the Dynamics 365 REST API.</p>

<p>Open the API Management service from the Azure portal, select: <em>APIs -&gt;  Add API -&gt; HTTP</em>:</p>

<p><img src="define-new-api.png" alt="APIM - Define new API" /></p>

<p>Select <em>Full</em>, Give the API a <em>Display name</em> (and <em>name</em>) and set the <em>Web service URL</em> to the REST API url of your dynamics instance:</p>

<p><img src="create-an-http-api.png" alt="Create HTTP API" /></p>

<p>From the API click <em>Add an operation</em> and fill in the <em>Display name</em>, <em>name</em> and <em>URL</em> and click <em>Save</em>.
The existing <a href="https://docs.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/whoami">WhoAmI</a> function is used as example here:</p>

<p><img src="add-operation.png" alt="Add WhoAmI operation" />
Note: the URL is case sensitive.</p>

<p>The final step is now to tell APIM to authenticate with its managed identity.</p>

<p>Open the Policy editor for the WhoAmI operation:
<img src="edit-policy.png" alt="" /></p>

<p>Add a <a href="https://docs.microsoft.com/en-us/azure/api-management/api-management-authentication-policies">authentication-managed-identity</a> policy to the inbound section with the <em>resource</em> attribute set to URI of you dynamics instance and save:
<img src="policy-editor.png" alt="" /></p>

<h2 id="4-testing-the-api-operation">4. Testing the API operation</h2>

<p>You should now be able to test the WhoAmI operation using the <em>Test</em> section:
<img src="test-operation.png" alt="" /></p>

<p>Assuming everything went according to the plan, you should receive a 200 OK response, and be ready to crank out your own API! 🥳</p>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="APIM" /><category term="Dynamics 365" /><category term="Power Platform" /><category term="Managed Identity" /><summary type="html"><![CDATA[Short and simple guide on how to connect APIM to Dynamics 365, using managed identities, and creating your first API endpoint.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/add-operation.png" /><media:content medium="image" url="/add-operation.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Associate and disassociate entities using the Xrm.WebApi Client API</title><link href="/2020/associate-disassociate-entities-xrm-webapi/" rel="alternate" type="text/html" title="Associate and disassociate entities using the Xrm.WebApi Client API" /><published>2020-03-12T00:00:00+01:00</published><updated>2020-03-12T00:00:00+01:00</updated><id>/2020/associate-disassociate-entities-xrm-webapi</id><content type="html" xml:base="/2020/associate-disassociate-entities-xrm-webapi/"><![CDATA[<p>With <a href="https://docs.microsoft.com/en-us/dynamics365-release-plan/2020wave1/">Dynamics 365: 2020 release wave 1</a> it now looks like it’s possible to associate and disassociate records using the Xrm.WebApi in the Client JavaScript API. Looking through the source code, I’ve come across this in the past, but it hasn’t been working in earlier releases.</p>

<p>The interesting bits from the source:</p>

<p><img src="xrm-webapi-source.png" alt="Xrm.WebApi source code showing Associate and Disassociate operations" /></p>

<p>As the <a href="https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi/online/execute">documentation</a> makes no mention of the Associate and Disassociate operations I’m unsure if they are officially supported or not. <strong><em>Use with caution!</em></strong></p>

<h2 id="usage">Usage:</h2>

<h3 id="associate-many-to-one-n1-relations"><strong>Associate Many-to-one (N:1) relations</strong></h3>

<p>I’ll get right to it, the syntax is pretty simple if you’ve used the <a href="https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi/online/execute">execute function</a> before:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">manyToOneAssociateRequest</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">getMetadata</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span>
        <span class="na">boundParameter</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="na">parameterTypes</span><span class="p">:</span> <span class="p">{},</span>
        <span class="na">operationType</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
        <span class="na">operationName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Associate</span><span class="dl">"</span>
    <span class="p">}),</span>
    <span class="na">relationship</span><span class="p">:</span> <span class="dl">"</span><span class="s2">account_primary_contact</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">target</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">entityType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">account</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2063dcca-6c63-ea11-a811-000d3aba6eaf</span><span class="dl">"</span>
    <span class="p">},</span>
    <span class="na">relatedEntities</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="na">entityType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">contact</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">c83aab0f-6863-ea11-a811-000d3aba6eaf</span><span class="dl">"</span>
        <span class="p">}</span>
    <span class="p">]</span>
<span class="p">}</span>

<span class="nx">Xrm</span><span class="p">.</span><span class="nx">WebApi</span><span class="p">.</span><span class="nx">online</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="nx">manyToOneAssociateRequest</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span>
    <span class="p">(</span><span class="nx">success</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Success</span><span class="dl">"</span><span class="p">,</span> <span class="nx">success</span><span class="p">);</span> <span class="p">},</span>
    <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error</span><span class="dl">"</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span> <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="associate-one-to-many-1n-relations"><strong>Associate One-to-many (1:N) relations</strong></h3>

<p>This is really just the reverse of N:1 relations:</p>

<p><img src="xrm-webapi-one-to-many.png" alt="Xrm.WebApi code example showing One-to-many association" /></p>

<h3 id="many-to-many-association"><strong>Many-to-many association</strong></h3>

<p>I think this is the most interesting use case, as this allows association of multiple entities in one request. This goes beyond what’s supported in the <a href="https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/webapi/associate-disassociate-entities-using-web-api">“raw” Web API</a>.</p>

<p><em>Note: In the example below I’ve created a custom entity named jaj_role. This has a many-to-many relationship to contact named jaj_role_contact.</em></p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">manyToManyAssociateRequest</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">getMetadata</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span>
        <span class="na">boundParameter</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="na">parameterTypes</span><span class="p">:</span> <span class="p">{},</span>
        <span class="na">operationType</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
        <span class="na">operationName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Associate</span><span class="dl">"</span>
    <span class="p">}),</span>
    <span class="na">relationship</span><span class="p">:</span> <span class="dl">"</span><span class="s2">jaj_role_contact</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">target</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">entityType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">contact</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">c83aab0f-6863-ea11-a811-000d3aba6eaf</span><span class="dl">"</span>
    <span class="p">},</span>
    <span class="na">relatedEntities</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="na">entityType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">jaj_role</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">aae3175e-cb63-ea11-a811-000d3aba6eaf</span><span class="dl">"</span>
        <span class="p">},</span>
        <span class="p">{</span>
            <span class="na">entityType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">jaj_role</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">ace3175e-cb63-ea11-a811-000d3aba6eaf</span><span class="dl">"</span>
        <span class="p">}</span>
    <span class="p">]</span>
<span class="p">}</span>

<span class="nx">Xrm</span><span class="p">.</span><span class="nx">WebApi</span><span class="p">.</span><span class="nx">online</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="nx">manyToManyAssociateRequest</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span>
    <span class="p">(</span><span class="nx">success</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Success</span><span class="dl">"</span><span class="p">,</span> <span class="nx">success</span><span class="p">);</span> <span class="p">},</span>
    <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error</span><span class="dl">"</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span> <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="disassociation"><strong>Disassociation</strong></h3>

<p>Sadly, it looks like disassociate only supports one disassociation per request.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">disassociateRequest</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">getMetadata</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span>
        <span class="na">boundParameter</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="na">parameterTypes</span><span class="p">:</span> <span class="p">{},</span>
        <span class="na">operationType</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
        <span class="na">operationName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Disassociate</span><span class="dl">"</span>
    <span class="p">}),</span>
    <span class="na">relationship</span><span class="p">:</span> <span class="dl">"</span><span class="s2">account_primary_contact</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">target</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">entityType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">contact</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">c83aab0f-6863-ea11-a811-000d3aba6eaf</span><span class="dl">"</span>
    <span class="p">},</span>
    <span class="na">relatedEntityId</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2063dcca-6c63-ea11-a811-000d3aba6eaf</span><span class="dl">"</span>
<span class="p">}</span>

<span class="nx">Xrm</span><span class="p">.</span><span class="nx">WebApi</span><span class="p">.</span><span class="nx">online</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="nx">disassociateRequest</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span>
    <span class="p">(</span><span class="nx">success</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Success</span><span class="dl">"</span><span class="p">,</span> <span class="nx">success</span><span class="p">);</span> <span class="p">},</span>
    <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Error</span><span class="dl">"</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span> <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h2 id="bonus-tip-1"><strong>Bonus tip #1:</strong></h2>

<p>Since we are using the Xrm.WebApi.online.execute function it should also be possible to batch the requests using the <a href="https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi/online/executemultiple">Xrm.WebApi.online.executeMultiple</a> function.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">requests</span> <span class="o">=</span> <span class="p">[</span><span class="nx">req1</span><span class="p">,</span> <span class="nx">req2</span><span class="p">,</span> <span class="nx">req3</span><span class="p">];</span>
<span class="nx">Xrm</span><span class="p">.</span><span class="nx">WebApi</span><span class="p">.</span><span class="nx">online</span><span class="p">.</span><span class="nx">executeMultiple</span><span class="p">(</span><span class="nx">requests</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">successCallback</span><span class="p">,</span> <span class="nx">errorCallback</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="bonus-tip-2"><strong>Bonus tip #2:</strong></h2>

<p>Looking through the code there is also mentions of the operations RetriveWithUrlParams and EntityDefinitions. I’ll leave it to you to figure those out 😊</p>

<p><img src="xrm-webapi-bonus-operations.png" alt="Source code showing RetriveWithUrlParams and EntityDefinitions operations" /></p>

<h2 id="happy-coding">Happy coding!</h2>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="Dynamics 365" /><category term="Power Platform" /><category term="JavaScript" /><category term="Xrm.WebApi" /><summary type="html"><![CDATA[How to associate and disassociate records using the Xrm.WebApi.online.execute function in the Client JavaScript API.]]></summary></entry><entry><title type="html">Hacking #dyn365 scripts like a boss😎</title><link href="/2020/hacking-dyn365-scripts-like-a-boss/" rel="alternate" type="text/html" title="Hacking #dyn365 scripts like a boss😎" /><published>2020-03-08T00:00:00+01:00</published><updated>2020-03-08T00:00:00+01:00</updated><id>/2020/hacking-dyn365-scripts-like-a-boss</id><content type="html" xml:base="/2020/hacking-dyn365-scripts-like-a-boss/"><![CDATA[<p><em>Disclaimer: This is by no means a new trick, it’s not limited to Dynamics 365 and I’m certainly not the first one to <a href="https://www.trysmudford.com/blog/chrome-local-overrides/">write about it</a>. However, it saves me a lot of time when debugging or prototyping, and since I still run into people who haven’t seen it, I thought I should share!</em></p>

<h2 id="tldr">tl;dr</h2>

<p>Use Chrome Local Overrides to edit JavaScript directly in the browser:</p>

<p><img src="chrome-local-overrides.gif" alt="Chrome Developer Tools showing Local Overrides with a JavaScript file open for editing" /></p>

<h2 id="long-version">Long version</h2>

<ol>
  <li>Open Chrome Developer Tools</li>
  <li>Click on the “Sources” tab</li>
  <li>Click “Show navigator”, if it’s not expanded</li>
  <li>Select the “Overrides” tab</li>
  <li>Click “Select folder for override” to select which folder to store local overrides</li>
  <li>Allow Chrome access to the folder</li>
  <li>Reload the browser window</li>
  <li>Use Ctrl + P and search for the JavaScript file you want to edit</li>
  <li>Do your edits</li>
  <li>Use Ctrl + S to save the file (locally)</li>
  <li>Reload the browser window to watch the changes take effect</li>
</ol>

<h2 id="bring-your-own-editor">Bring your own editor</h2>

<p>Chrome also listens to local changes to the overridden files. This means that you can also use another editor to make changes to the files.</p>

<p><img src="bring-your-own-editor.gif" alt="Dynamics 365 form side by side with Chrome DevTools, editing an overridden JavaScript file" /></p>

<p>My thoughts about using this feature:</p>

<h2 id="the-good">The good</h2>

<ul>
  <li>It cuts down the developer feedback loop – just save and reload. No need to edit, transpile, upload, publish, reload, test.</li>
  <li>You have all the tooling readily available, right inside Chrome. No need to open / install Visual Studio, Git, XrmToolkit, etc.</li>
  <li>Since you are only making changes to your local environment, you don’t risk wrecking anything for other people working in the same solution. This can be especially handy if you have to do some work directly in production… 🙀</li>
</ul>

<h2 id="the-bad">The bad</h2>

<ul>
  <li>Doesn’t fit very well with workflows using TypeScript, or other ES variants, which needs to be transpiled.</li>
  <li>It smells like an anti-pattern, and since your changes aren’t backed by source control it’s easy to lose track of changes. Therefor I would try to limit this trick to debugging and prototyping.</li>
</ul>

<h2 id="the-ugly">The ugly</h2>

<ul>
  <li>Every time you publish a new version of the script in Dynamics, the resource path (URI) is updated. This leads to Chrome losing the link between the remote and local file, and you must store a new file, locally, if you want to make future edits. This makes integration with build tools and source control difficult.</li>
</ul>

<h3 id="so-be-aware-of-the-pitfalls-and-keep-on-hacking-in-the-free-world-">So be aware of the pitfalls, and keep on hacking in the free world 🐱‍💻!</h3>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="Dynamics 365" /><category term="JavaScript" /><category term="Chrome" /><category term="Debugging" /><summary type="html"><![CDATA[Use Chrome Local Overrides to edit Dynamics 365 JavaScript directly in the browser — cutting down the developer feedback loop.]]></summary></entry><entry><title type="html">A structured approach to testing JavaScript for the Unified Interface transition</title><link href="/2020/structured-approach-testing-javascript-unified-interface/" rel="alternate" type="text/html" title="A structured approach to testing JavaScript for the Unified Interface transition" /><published>2020-03-02T00:00:00+01:00</published><updated>2020-03-02T00:00:00+01:00</updated><id>/2020/structured-approach-testing-javascript-unified-interface</id><content type="html" xml:base="/2020/structured-approach-testing-javascript-unified-interface/"><![CDATA[<p><img src="dynamics-365-header.jpg" alt="Microsoft Dynamics 365 header" /></p>

<p>With Microsoft Dynamics 365 final transition date, October 2020, to Unified Interface closing in, it is high time getting your JavaScripts ready for the switch.</p>

<p>If you have large solution, and especially if it’s been developed over several version of Dynamics, the task can be substantial, and a structured approach is indeed needed.</p>

<h2 id="power-apps-solution-checker-doesnt-cut-it">Power Apps Solution Checker doesn’t cut it</h2>

<p>While the Solution Checker can provide helpful tips about best practices and future deprecations, our experience so far, has been that it generates a lot of noise, but doesn’t actually catch (subtle) changes that breaks scripts.</p>

<p>On example of this is the setValue function. The old legacy web client is quite forgiving and accepts both string and numbers as inputs for a Two Options field. The new UI, however throws an error if you try using anything but a Boolean.</p>

<p>Setting a two options field with a string in the (now) old legacy client works without problems.</p>

<p><img src="setvalue-legacy-client.png" alt="Setting a two options field with a string in the legacy web client" /></p>

<p>Doing the same in Unified Interface throws an error!</p>

<p><img src="setvalue-unified-interface-error.png" alt="Setting a two options field in Unified Interface throws an error" /></p>

<p>Another example is the Xrm.WebApi.retrieveRecord (and related) functions. The entity logical name is accepted in both singular and plural form in the legacy client. In the new Unified Interface only the singular form is recognized:</p>

<p><img src="retrieverecord-entity-name.png" alt="Xrm.WebApi.retrieveRecord entity logical name differences between legacy and Unified Interface" /></p>

<p>The solution checker won’t warn about any of these, or other, problems. So, if you want to make sure that your solution continues to work in Unified Interface, you need to do a proper GAP analyses and really test that your scripts continue to work properly.</p>

<h2 id="create-an-inventory-of-javascripts-and-functions">Create an inventory of JavaScripts and functions</h2>

<p>Before you can start testing, you need to know what lives inside your solution; scripts, functions and the events triggering the functions. The best tool we’ve found for the job is Scripts Finder in XrmToolBox.</p>

<p><img src="scripts-finder-xrmtoolbox.png" alt="Scripts Finder in XrmToolBox" /></p>

<p>Using the tool is quite easy:</p>

<ol>
  <li>Connect to your organization</li>
  <li>Start Scripts Finder</li>
  <li>Click Find Scripts usage and select For all entities</li>
  <li>Watch the spinner for some time…</li>
  <li>Click Export to csv</li>
  <li>Open the file in Excel</li>
  <li>Use the Data → Text to Columns function if the file content isn’t automatically recognized</li>
  <li>Use the Home → Sort &amp; Filter → Filter command to enable sorting and filtering in the first row</li>
</ol>

<p>You should now have an almost (more on that later…) complete list of JavaScript usage in your Solution:</p>

<p><img src="scripts-finder-results.png" alt="Scripts Finder results exported to Excel showing JavaScript inventory" /></p>

<h3 id="doing-some-filtering">Doing some filtering…</h3>

<p>As the list contains (almost) all the scripts in the solution you should filter out those which aren’t relevant for your testing.</p>

<p><img src="scripts-finder-filtering.png" alt="Filtering the Scripts Finder results in Excel" /></p>

<ul>
  <li>Filter out built-in and third-party scripts by opening the filtering drop-down on the Script Location Column and deselect unwanted scripts.</li>
  <li>Filter out scripts on inactive forms in the Form State column.</li>
  <li>Filter out disabled script functions in the Enabled column.</li>
</ul>

<h2 id="testing">Testing</h2>

<p>You are now ready to start testing, which means starting on the top of the list and test each function in Unified Interface.</p>

<p>Going down the list you should make a note of the following columns:</p>

<ul>
  <li><strong>Entity Logical Name</strong> – tells you which entity to test</li>
  <li><strong>Form Name</strong> – tells you which form to test</li>
  <li><strong>Event</strong> – tells you which event that will triggers the function</li>
  <li><strong>Control</strong> – tells you which control (if any) triggers the function</li>
  <li><strong>Script Location</strong> – tells you which script to look for</li>
  <li><strong>Method Called</strong> – tells you the method you should test</li>
</ul>

<h3 id="example-testing-session-for-account">Example testing session for account</h3>

<p><img src="testing-session-account.png" alt="Example testing session showing script functions for the account entity" /></p>

<p>Looking at the above image: for the Sales Insights form on the account entity we should test the setCreditonhold function and loadAnotherAccount function in the lv_unified_breakage.js file. (The last line just tells us that lv_unified_breakage.js is added to the form).</p>

<p>Fire up Chrome (or a similar chromium browser) and open the Sales Insights form on the account entity.</p>

<p>Open developer tools and press CTRL-P and search for the script file and select it:</p>

<p><img src="devtools-search-script.png" alt="Chrome DevTools - searching for a script file with CTRL-P" /></p>

<p>The script file will open breakpoints can be added to the setCreditonhold function and loadAnotherAccount function:</p>

<p><img src="devtools-breakpoints.png" alt="Chrome DevTools - adding breakpoints to JavaScript functions" /></p>

<p>Setting breakpoints will allow stepping though the functions when they are triggered and verify that they work as intended. For functions with branching logic you would typically add breakpoints to each branch and test them individually.</p>

<p>All that is left is to trigger the setCreditonhold function by loading the form and the loadAnotherAccount function by changing the product and wish you:</p>

<p>Happy testing 😊</p>

<h2 id="bonus-tip-1--look-out-for-event-handlers-added-through-code">Bonus tip #1 – Look out for event handlers added through code</h2>

<p>The report from Scripts Finder does not include event handlers added through code with addOnChange, addOnLoad, etc. so remember to look for them when going through script files.</p>

<h2 id="bonus-tip-2--web-resources">Bonus tip #2 – Web resources</h2>

<p>JavaScript added to web resources (as files or inline) are not included in the report from Scripts Finder so remember to test them as well.</p>

<h2 id="bonus-tip-3--dealing-with-badly-formatted--minimized-scripts">Bonus tip #3 – Dealing with badly formatted / “minimized” scripts</h2>

<p>Chrome pretty print function can help you deal with this:</p>

<p><img src="devtools-pretty-print.png" alt="Chrome DevTools pretty print function for formatting minimized scripts" /></p>

<hr />

<p><em>Originally published on <a href="https://www.linkedin.com/pulse/structured-approach-testing-javascript-unified-jan-%C3%A5ge-johnsen/">LinkedIn</a>.</em></p>]]></content><author><name>Jan-Åge Johnsen</name><email>jan.age.johnsen@gmail.com</email></author><category term="Dynamics 365" /><category term="JavaScript" /><category term="Unified Interface" /><category term="Dynamics 365" /><category term="Testing" /><summary type="html"><![CDATA[A structured approach to testing and preparing your JavaScript customizations for the Microsoft Dynamics 365 Unified Interface transition.]]></summary></entry></feed>