<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://hajekj.net/feed/index.xml" rel="self" type="application/atom+xml" /><link href="https://hajekj.net/" rel="alternate" type="text/html" /><updated>2026-04-01T23:20:04+02:00</updated><id>https://hajekj.net/feed/index.xml</id><title type="html">Honza’s Blarg</title><subtitle>Things that didn&apos;t fit elsewhere...</subtitle><author><name>Jan Hajek</name></author><entry><title type="html">Debugging Dataverse plugins</title><link href="https://hajekj.net/2026/03/09/debugging-dataverse-plugins/" rel="alternate" type="text/html" title="Debugging Dataverse plugins" /><published>2026-03-09T11:20:00+01:00</published><updated>2026-03-09T11:20:00+01:00</updated><id>https://hajekj.net/2026/03/09/debugging-dataverse-plugins</id><content type="html" xml:base="https://hajekj.net/2026/03/09/debugging-dataverse-plugins/"><![CDATA[<p>I haven’t seen many developers debug plugins in Dataverse. Since Dataverse runs in cloud, you can’t just attach debugger to the remote server like you would do in App Service and debug. There are some ways to do it locally like <a href="https://github.com/DynamicsValue/fake-xrm-easy">FakeXrmEasy</a>, but when you want to debug the end to end in real environment, there are some differences and extra steps which you need to take.</p>

<!-- more -->

<p>I have seen way too many people rely on tracing (which brought back some 2008-ish PHP memories), so just thought I would summarize the whole process here.</p>

<p>First off, let’s start with <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/debug-plug-in">docs</a>. Tldr; you have to use the <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/register-plug-in#about-the-plug-in-registration-tool">Plugin Registration Tool (PRT)</a> (<code class="language-plaintext highlighter-rouge">pac tool prt --update</code> - make sure you have the latest version).</p>

<p>Next, build your plugin in Debug configuration (simple <code class="language-plaintext highlighter-rouge">dotnet build</code> does the job usually). This process works for both <a href="https://github.com/gluck/il-repack">ILRepack</a> and also for <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/build-and-package#dependent-assemblies">plugin packages</a>.</p>

<blockquote>
  <p>If you are using ILRepack, ensure that you also get correct PDB file. I had to configure output to a different folder in order for proper PDB to be generated. Without correct PDB, you won’t be able to debug the code.</p>
</blockquote>

<p>Before launching PRT, navigate to <code class="language-plaintext highlighter-rouge">%LOCALAPPDATA%\Microsoft\PowerPlatform\PRT\9.1.0.200\tools</code> (the version will change in time) and modify <code class="language-plaintext highlighter-rouge">appsettings.json</code> to have <code class="language-plaintext highlighter-rouge">LegacyPluginProfiler</code> set to <code class="language-plaintext highlighter-rouge">false</code> instead of <code class="language-plaintext highlighter-rouge">true</code>. This step is very important if you want to debug plugin packages, and I haven’t found any reason why not to have this enabled for any kind of debugging. After this, launch PRT (<code class="language-plaintext highlighter-rouge">pac tool prt</code>). This step is described in <a href="https://learn.microsoft.com/fr-fr/power-apps/developer/data-platform/dependent-assembly-plugins#profileur-de-plug-ins">French docs</a> (and few other languages) but not in any English version.</p>

<blockquote>
  <p>You have to disable the LegacyPluginProfile in order to be able to debug plugin packages, if you use the legacy one, you will get <code class="language-plaintext highlighter-rouge">Unexpected Exception in the Plug-in Profiler</code> error in the API and some more details in the trace log since plugin packages are stored differently than plugin DLLs.</p>
</blockquote>

<p>Now import your plugin/package and create the steps and do all the usual stuff. Make sure that <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/logging-tracing#enable-trace-logging">Plugin Trace Log is enabled</a> (you can do this in PRT too).</p>

<p>Next you can simply follow the <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/tutorial-debug-plug-in?tabs=prt">docs</a> - <em>Start Profiling</em> (you won’t see the <em>Profile Settings</em> window if you configured PRT correctly). Perform an action to execute the plugin. Then you can just go to <em>Debug</em>, select the execution from the history, and then select the assembly.</p>

<p>Finally, you can just <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/tutorial-debug-plug-in?tabs=prt#debug-your-plug-in"><em>Attach to Process</em> from Visual Studio</a> and replay the execution. If you are using VS Code, create <a href="https://code.visualstudio.com/docs/debugtest/debugging-configuration#_launchjson-attributes"><em>launch.json</em></a> and set the following (note the type is set to <code class="language-plaintext highlighter-rouge">clr</code> instead of <code class="language-plaintext highlighter-rouge">coreclr</code> because you are debugging .NET Framework):</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">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0.2.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"configurations"</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">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">".NET Attach"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"clr"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"request"</span><span class="p">:</span><span class="w"> </span><span class="s2">"attach"</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>Have fun debugging!</p>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Microsoft Power Platform" /><category term="Dataverse" /><category term="Debugging" /><summary type="html"><![CDATA[I haven’t seen many developers debug plugins in Dataverse. Since Dataverse runs in cloud, you can’t just attach debugger to the remote server like you would do in App Service and debug. There are some ways to do it locally like FakeXrmEasy, but when you want to debug the end to end in real environment, there are some differences and extra steps which you need to take.]]></summary></entry><entry><title type="html">Running a Remote Desktop Gateway</title><link href="https://hajekj.net/2026/01/27/running-a-remote-desktop-gateway/" rel="alternate" type="text/html" title="Running a Remote Desktop Gateway" /><published>2026-01-27T22:20:00+01:00</published><updated>2026-01-27T22:20:00+01:00</updated><id>https://hajekj.net/2026/01/27/running-a-remote-desktop-gateway</id><content type="html" xml:base="https://hajekj.net/2026/01/27/running-a-remote-desktop-gateway/"><![CDATA[<p>I am a heavy user of Remote Desktop. I connect to my work computer from my MacBook for coding, debugging, doing demos, sometimes even gaming. In some cases, I connect even from my phone. For remote access, I am using a combination of either <a href="https://guacamole.apache.org/">Apache Guacamole</a> in the browser (it’s amazing, but there are some frustrating things like copying images to session or some shortcuts - Windows X MacOS), <a href="https://www.cloudflare.com/zero-trust/products/access/">Cloudflare Access</a> in combination with a native RDP application (either MSTSC or the <a href="https://learn.microsoft.com/en-us/windows-app/get-started-connect-devices-desktops-apps?tabs=windows-avd%2Cwindows-w365%2Cwindows-devbox%2Cmacos-rds%2Cmacos-pc&amp;pivots=remote-pc">Windows app</a>) or sometimes <a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/rdp/rdp-cloudflared-authentication/">cloudflared</a> in combination with Bastion setup. Recently, I was looking into hosting my own <a href="https://learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/remote-desktop-gateway-role">Remote Desktop Gateway</a> to be able to connect to a machine behind a firewall without the need to be in a VPN (Remote Desktop Gateway masks the RDP traffic as <a href="https://learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/rds-roles#remote-desktop-gateway">HTTPS connection</a>).</p>

<!-- more --->

<p>In the past I have done many <a href="https://learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/overview">Remote Desktop Service</a> deployments, some of which are still running to date. The first requirement however is, I don’t want to host a Windows Server. Second, I don’t want to manage an Active Directory either (it is a <a href="https://learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/rds-plan-build-anywhere">requirement</a>). Third, I preferably want to deploy this on my existing server and preferrably as a Docker container.</p>

<p>So I started searching around and found out that there are a few (not really well known) implementations of the Remote Desktop Gateway and its protocol:</p>
<ul>
  <li><a href="https://github.com/mKenfenheuer/rdpgw.net/">https://github.com/mKenfenheuer/rdpgw.net/</a> (C#, also has a <a href="https://github.com/mKenfenheuer/ksol-rdpgw">working sample</a>)</li>
  <li><a href="https://github.com/bolkedebruin/rdpgw">https://github.com/bolkedebruin/rdpgw</a> (Go)</li>
  <li><a href="https://github.com/gabriel-sztejnworcel/pyrdgw">https://github.com/gabriel-sztejnworcel/pyrdgw</a> (Python)</li>
</ul>

<p>All of these libraries seem to implement it in a similar fashion, however I found the RDPGW in Go the most complete - it also ships as a Docker container, which you can just use - the only issue is that the documentation is somewhat not complete and some things are not really working - yet.</p>

<p>With this implementation, you can <a href="https://github.com/bolkedebruin/rdpgw?tab=readme-ov-file#authentication">choose between</a> <em>openid</em>, <em>local</em> (Basic authentication), <em>ntlm</em>, <em>headers</em> (when behind Cloudflare Access or Azure App Proxy) and <em>kerberos</em>. Initially I wanted to go with NTLM authentication, because you can easily configure it from each of the clients as gateway credentials, however I have hit two issues - <a href="https://developers.cloudflare.com/dns/proxy-status/limitations/#windows-authentication">Cloudflare doesn’t support proxying NTLM authentication</a> (and I want it published through <a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/">Cloudflare Tunnel</a>) and NGINX doesn’t support NTLM either (only as part of <a href="https://nginx.org/en/docs/http/ngx_http_upstream_module.html#ntlm">commercial subscription</a>, there is a <a href="https://github.com/gabihodoroaga/nginx-ntlm-module">free and OSS module</a> but I haven’t tried it, since I want it behind Cloudflare anyways) - I use NGINX as an ingress proxy for all containers and services I host. The next choice was the basic authentication option - and while this would be pretty much suitable for me, <a href="https://github.com/bolkedebruin/rdpgw/blob/master/docs/pam-authentication.md#compatible-clients">it doesn’t work with native MSTSC client</a> (documentation says that <a href="https://learn.microsoft.com/en-us/sysinternals/downloads/rdcman">RDCMan</a> works, but I didn’t manage to get it working either - RDCMan is just a GUI on top of MSTSC anyways), despites even explicitly setting <a href="https://learn.microsoft.com/en-us/azure/virtual-desktop/rdp-properties#gatewaycredentialssource"><code class="language-plaintext highlighter-rouge">gatewaycredentialssource</code></a> to <code class="language-plaintext highlighter-rouge">3</code>. Next, I configured it for header authentication, which is however <a href="https://github.com/bolkedebruin/rdpgw/issues/164">crashing due to a little bug</a>.</p>

<p>The last option was to go with <a href="https://github.com/bolkedebruin/rdpgw/blob/master/docs/openid-authentication.md">OpenID Connect setup</a>. This method worked across all platforms, and let me go a little bit into how it works: First, you have to visit <code class="language-plaintext highlighter-rouge">https://rdgateway.domain.com/connect?host=xxx.corp.domain.com:3389</code> and authenticate, which in return will give you a <code class="language-plaintext highlighter-rouge">.rdp</code> file preauthenticated to the gateway. You then open the file in MSTSC or Windows (on MacOS or iOS) apps, and authenticate to the target computer. The authentication token to the gateway is shortlived, so in case you need to reconnect, you have to download the file again.</p>

<p>The authentication token is stored in <code class="language-plaintext highlighter-rouge">gatewayaccesstoken</code> property in the <code class="language-plaintext highlighter-rouge">.rdp</code> file itself (it is a shortlived - 5 minutes - JWT token), which the gateway verifies and let’s you connect. It is also refered to as the <a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/dff3285e-05de-483b-950b-8c6388e55713"><em>PAA Cookie</em></a>. So thanks to this, you can easily connect to your machine from native client, from anywhere without the need for a VPN or running cloudflared.</p>

<p>The only disappointing thing is, that there’s no interactive way to continuously obtain the <code class="language-plaintext highlighter-rouge">gatewayaccesstoken</code> from an IdP in some modern way (so you could skip the entire download a RDP file thing).</p>

<p>I am currently looking into ways to either contribute to the RDGW in Go to fix some of the quirks with the header authentication or leveraging the C# implementation to make my own gateway server in the future.</p>

<p>I also have eyes on <a href="https://techcommunity.microsoft.com/blog/azurevirtualdesktopblog/announcing-new-hybrid-deployment-options-for-azure-virtual-desktop/4468781">Azure Virtual Desktop hybrid deployment</a>. It appears to be in private preview (and the form is already closed). It could remove the need for hosting the gateway completely, since the Arc-enabled machine could be acessibly from AVD directly. I hope to learn more about it soon.</p>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Microsoft Azure" /><category term="Open Source" /><category term="Docker" /><category term="Azure AD" /><category term="Remote Desktop" /><summary type="html"><![CDATA[I am a heavy user of Remote Desktop. I connect to my work computer from my MacBook for coding, debugging, doing demos, sometimes even gaming. In some cases, I connect even from my phone. For remote access, I am using a combination of either Apache Guacamole in the browser (it’s amazing, but there are some frustrating things like copying images to session or some shortcuts - Windows X MacOS), Cloudflare Access in combination with a native RDP application (either MSTSC or the Windows app) or sometimes cloudflared in combination with Bastion setup. Recently, I was looking into hosting my own Remote Desktop Gateway to be able to connect to a machine behind a firewall without the need to be in a VPN (Remote Desktop Gateway masks the RDP traffic as HTTPS connection).]]></summary></entry><entry><title type="html">Exporting Dataverse audit to Data Lake</title><link href="https://hajekj.net/2026/01/14/exporting-dataverse-audit-to-data-lake/" rel="alternate" type="text/html" title="Exporting Dataverse audit to Data Lake" /><published>2026-01-14T12:30:00+01:00</published><updated>2026-01-14T12:30:00+01:00</updated><id>https://hajekj.net/2026/01/14/exporting-dataverse-audit-to-data-lake</id><content type="html" xml:base="https://hajekj.net/2026/01/14/exporting-dataverse-audit-to-data-lake/"><![CDATA[<p>Dataverse has very powerful <a href="https://learn.microsoft.com/en-us/power-platform/admin/manage-dataverse-auditing">auditing</a>, however, if you are running a larger system, you may quickly <a href="https://learn.microsoft.com/en-us/power-platform/admin/capacity-storage">run out of log storage</a> which will then start <a href="https://learn.microsoft.com/en-us/power-platform/admin/capacity-storage#example-storage-capacity-scenario-no-overage">consuming your database storage</a>. The log storage is quite expensive (9.87EUR per GB per month) so let’s look at an easy way to offload your audit logs.</p>

<!-- more -->

<p>There are already GUI tools to export the <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/entities/audit">Audit table</a> - some available through <a href="https://www.xrmtoolbox.com/">XrmToolbox</a> but we want this to be done automatically in the background without having to do manual cleanup. And we can achieve this via export to Data Lake Storage in Azure. While there is <a href="https://learn.microsoft.com/en-us/power-platform/admin/audit-data-azure-synapse-link">already a guide for this</a> it requires you to deploy Azure Synapse Workspace and an Apache Spark pool which will incur additional costs but you can do it without all of that.</p>

<p>First, you need to decide on what you plan to use those audit logs for - if you have them just as a backup, or access them in a very infrequent way, you can safely offload the audit elsewhere. You can offload everything and then for example keep 1 year of audit in Dataverse for easy access, and over that in Data Lake for long-term retention. Remember that once you offload the audit, it will be in the <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/azure-synapse-link-data-lake#view-your-data-in-azure-data-lake-storage-gen2">CSV format</a> in the Data Lake, so you will need another tool (like Power BI or actual Azure Synapse Link) to search through the data easily. So let’s get it running:</p>

<p>First, <a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal">create a Data Lake storage account</a> in Azure, preferrably in the same region as your Dataverse and make sure to enable <a href="https://learn.microsoft.com/en-us/azure/storage/blobs/data-lake-storage-namespace"><em>Hierarchical Namespace</em></a> (you can follow <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/azure-synapse-link-data-lake#prerequisites">this</a> guide). Next, proceed with connecting the Data Lake to Dataverse, you can follow <a href="https://learn.microsoft.com/en-us/power-apps/maker/data-platform/azure-synapse-link-data-lake#connect-dataverse-to-azure-data-lake-storage-gen2">these steps</a>, however you will notice, that there is no Auditing table available. That’s fine, just select some other table (preferrably some table which you are not using like <code class="language-plaintext highlighter-rouge">adx_ads</code>), save it, and wait for it to complete.</p>

<p>Next, open <a href="https://learn.microsoft.com/en-us/microsoft-edge/devtools/overview">F12 Developer Tools</a> and select the <a href="https://learn.microsoft.com/en-us/microsoft-edge/devtools/network/">Network</a> tab. With those tools open, navigate to the Data Lake configuration, and find the GET request going to the <em>athenawebservice.*.gateway.prod.island.powerapps.com</em>. From the GET request’s body and headers, you need to populate the code below:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;url&gt;</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// https://athenawebservice.neu-il103.gateway.prod.island.powerapps.com/environment/2b780552-3247-473b-ba66-e3681799d66f/lakeprofile/39ee4c30-8487-48e6-b476-a55731f66171</span>
<span class="kd">const</span> <span class="nx">authorization</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;authorization_header_value&gt;</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// Bearer ...</span>
<span class="kd">const</span> <span class="nx">id</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;id&gt;</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// GUID</span>
<span class="kd">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;name&gt;</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// name</span>
<span class="kd">const</span> <span class="nx">organizationId</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;organization_id&gt;</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// GUID</span>
<span class="kd">const</span> <span class="nx">organizationUrl</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;organization_url&gt;</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// URL</span>
<span class="kd">const</span> <span class="nx">lakeId</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;lake_id&gt;</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// GUID</span>

<span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
  <span class="dl">"</span><span class="s2">headers</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">accept</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">*/*</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">authorization</span><span class="dl">"</span><span class="p">:</span> <span class="nx">authorization</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">cache-control</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">no-cache</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="dl">"</span><span class="s2">body</span><span class="dl">"</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span>
    <span class="dl">"</span><span class="s2">Id</span><span class="dl">"</span><span class="p">:</span> <span class="nx">id</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">Version</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1.0</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">State</span><span class="dl">"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">LastModified</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2023-11-20T22:10:17</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">Name</span><span class="dl">"</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">OrganizationId</span><span class="dl">"</span><span class="p">:</span> <span class="nx">organizationId</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">OrganizationUrl</span><span class="dl">"</span><span class="p">:</span> <span class="nx">organizationUrl</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">Entities</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="dl">"</span><span class="s2">Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">audit</span><span class="dl">"</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">EntitySource</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Dataverse</span><span class="dl">"</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">AppendOnlyMode</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">PartitionStrategy</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Month</span><span class="dl">"</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">RecordCountPerBlock</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">Settings</span><span class="dl">"</span><span class="p">:</span> <span class="p">{}</span>
        <span class="p">}</span>
    <span class="p">],</span>
    <span class="dl">"</span><span class="s2">DestinationType</span><span class="dl">"</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">DestinationKeyVaultUri</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">dummy</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">DestinationPrefix</span><span class="dl">"</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">LakeId</span><span class="dl">"</span><span class="p">:</span> <span class="nx">lakeId</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">RetryPolicy</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">"</span><span class="s2">MaxRetryCount</span><span class="dl">"</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">IntervalInSeconds</span><span class="dl">"</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">Backoff</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span>
    <span class="p">},</span>
    <span class="dl">"</span><span class="s2">Status</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">"</span><span class="s2">ExportStatus</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Success</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">InitialSyncState</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Completed</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">TotalNotifications</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">SuccessNotifications</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">FailureNotifications</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">LastExportDate</span><span class="dl">"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">LastModifiedOn</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2023-11-20T22:10:17</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">MetadataState</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Created</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">ForceRefreshState</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">NotStarted</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">LastForceRefreshRequestTime</span><span class="dl">"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">LastForceRefreshStartTime</span><span class="dl">"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">LastForceRefreshEndTime</span><span class="dl">"</span><span class="p">:</span> <span class="kc">null</span>
    <span class="p">},</span>
    <span class="dl">"</span><span class="s2">WriteDeleteLog</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">CountFeature</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">CreationTime</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2023-11-20T22:10:17</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">ActivationTime</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2022-04-11T12:26:19</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">UpdateTime</span><span class="dl">"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">DestinationSchemaName</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">dbo</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">NeedCopyAttachmentsToBlob</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">NeedCopyFileTypeAttachmentsToBlob</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">EnabledForJobs</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">EnabledForIncrementalUpdate</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">EnabledForDeltaLake</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">EnabledForDlw</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">IncrementalUpdateTimeInterval</span><span class="dl">"</span><span class="p">:</span> <span class="mi">60</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">SynapseSyncState</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">NotStarted</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">LinkedToFabric</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">EnabledForFnOTablesBaseEnumSupport</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">IsDeDuplicationJobsSubmitted</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">LinkedFnOEnvironmentLastObservedValue</span><span class="dl">"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">ShouldSkipShortCutsDeletionInOLCFlow</span><span class="dl">"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">ProfilePauseResumeOperationsType</span><span class="dl">"</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">LinkedFnOEnvironmentCurrentAOSCount</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">LinkedFnOEnvironmentAdditionalAOSCount</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span>
  <span class="p">}),</span>
  <span class="dl">"</span><span class="s2">method</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PUT</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">mode</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">cors</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">credentials</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">include</span><span class="dl">"</span>
<span class="p">});</span>

<span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">/activate`</span><span class="p">,</span> <span class="p">{</span>
  <span class="dl">"</span><span class="s2">headers</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">accept</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">*/*</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">authorization</span><span class="dl">"</span><span class="p">:</span> <span class="nx">authorization</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">cache-control</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">no-cache</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="dl">"</span><span class="s2">body</span><span class="dl">"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">method</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">mode</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">cors</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">credentials</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">include</span><span class="dl">"</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Once you do that, you can copy paste it to the console in the developer tools and execute. It will update the export profile and active it, and in a minute the audit log will begin to be exported into the Data Lake.</p>

<blockquote>
  <p><strong>WARNING:</strong> Remember, you are executing a code from the Internet, so always make sure it is going to do what you expect it to do.</p>
</blockquote>

<p>Once the intial export finishes, you can then <a href="https://learn.microsoft.com/en-us/power-platform/admin/manage-dataverse-auditing#turn-on-auditing">configure the audit log retention</a> to a lower time in Dataverse and also <a href="https://learn.microsoft.com/en-us/power-platform/admin/manage-dataverse-auditing#delete-audit-logs">trigger the audit deletion</a> if you want to delete older logs immediatelly (more on that in another article).</p>

<p>And you are all set, audit is now persisted in much cheaper storage. I am kind of surprised that exporting audit to Data Lake directly without Azure Synapse Link is not supported, but at least it works this way.</p>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Microsoft Power Platform" /><category term="Microsoft Azure" /><category term="Dataverse" /><summary type="html"><![CDATA[Dataverse has very powerful auditing, however, if you are running a larger system, you may quickly run out of log storage which will then start consuming your database storage. The log storage is quite expensive (9.87EUR per GB per month) so let’s look at an easy way to offload your audit logs.]]></summary></entry><entry><title type="html">Protecting 4th+ level domains with Cloudflare for free</title><link href="https://hajekj.net/2025/12/22/protecting-4th-level-domains-with-cloudflare-for-free/" rel="alternate" type="text/html" title="Protecting 4th+ level domains with Cloudflare for free" /><published>2025-12-22T12:00:00+01:00</published><updated>2025-12-22T12:00:00+01:00</updated><id>https://hajekj.net/2025/12/22/protecting-4th-level-domains-with-cloudflare-for-free</id><content type="html" xml:base="https://hajekj.net/2025/12/22/protecting-4th-level-domains-with-cloudflare-for-free/"><![CDATA[<p>I have been using Cloudflare since 2011 for DNS, caching and protection. In 2014 Cloudflare <a href="https://blog.cloudflare.com/introducing-universal-ssl/">introduced Universal SSL</a> helping enable HTTPS for millions of free sites for free. Universal SSL however covers only <a href="https://developers.cloudflare.com/ssl/edge-certificates/universal-ssl/limitations/#hostname-coverage">3rd level domains</a> (eg. <em>*.domain.com</em> and <em>domain.com</em>), and you need to purchase <a href="https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/">Advanced Certificate Manager</a> or upgrade to Business plan to bring your own certificate. I recently discovered a third way to do this, which is completely free (let’s hope it stays that way).</p>

<!-- more -->

<h2 id="my-scenario">My scenario</h2>

<p>I accidentally stumbled upon this “feature” yesterday, while I was trying to expose <a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/ssh/ssh-browser-rendering/">SSH browser rendering</a> for my Raspberry.</p>

<p>Basically, I wanted to expose it under <code class="language-plaintext highlighter-rouge">raspberry.ssh.hajekj.net</code>, however I ended up with <em>ERR_SSL_VERSION_OR_CIPHER_MISMATCH</em> in your browser (or similar), meaning that Cloudflare doesn’t have the corresponding certificate.</p>

<blockquote>
  <p>I am aware that I could publish it under <code class="language-plaintext highlighter-rouge">ssh-raspberry.hajekj.net</code> or something, but I just like the 4th level subdomain format better.</p>
</blockquote>

<h2 id="cloudflare-for-saas-to-the-rescue">Cloudflare for SaaS to the rescue!</h2>

<p>I have been doing a lot of experiments with <a href="https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/">Cloudflare for SaaS</a> as an alternative to <a href="https://learn.microsoft.com/en-us/azure/frontdoor/front-door-overview">Azure Front Door</a>. Since I had it setup on my domain, I just thought, let’s try to add the 4th level domain there (since Cloudflare for SaaS creates its own certificates) and see what happens.</p>

<p>I simply added the domain to DNS (via <a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/">Published application</a> from Cloudflare Tunnel, but orange clouded DNS record will work too) and also added the domain to Cloudflare for SaaS (Dashboard &gt; <em>SSL/TLS</em> &gt; <em>Custom Hostnames</em>),</p>

<p>After less than a minute, a certificate was issued and the host is published with a 4th level domain secured by Cloudflare’s certificate withou ACM (I just hope nobody disables this on Cloudflare’s side).</p>

<blockquote>
  <p>Some people actually call this a 3rd level domain, so I am just mentioning it down here for visibility.</p>
</blockquote>]]></content><author><name>Jan Hajek</name></author><category term="Cloudflare" /><summary type="html"><![CDATA[I have been using Cloudflare since 2011 for DNS, caching and protection. In 2014 Cloudflare introduced Universal SSL helping enable HTTPS for millions of free sites for free. Universal SSL however covers only 3rd level domains (eg. *.domain.com and domain.com), and you need to purchase Advanced Certificate Manager or upgrade to Business plan to bring your own certificate. I recently discovered a third way to do this, which is completely free (let’s hope it stays that way).]]></summary></entry><entry><title type="html">Two way communication in Power Automate with Service Bus</title><link href="https://hajekj.net/2025/12/19/two-way-communication-in-power-automate-with-service-bus/" rel="alternate" type="text/html" title="Two way communication in Power Automate with Service Bus" /><published>2025-12-19T17:00:00+01:00</published><updated>2025-12-19T17:00:00+01:00</updated><id>https://hajekj.net/2025/12/19/two-way-communication-in-power-automate-with-service-bus</id><content type="html" xml:base="https://hajekj.net/2025/12/19/two-way-communication-in-power-automate-with-service-bus/"><![CDATA[<p>When we need to communicate with on-premise environment, we usually utilize Service Bus. Up until now, most of the messages have been fire and forget, however recently, we had a situation where we needed to perform two way communication (basically request and response) to basically perform some calculation on top of on-prem data and return the data.</p>

<!-- more -->

<p>There are actually multiple solutions to this problem - we could go with <a href="https://learn.microsoft.com/en-us/data-integration/gateway/service-gateway-onprem">on-premises data gateway</a> (which would most-likely require running a web server at customer’s environment - and handling auth etc.), we could go with <a href="https://learn.microsoft.com/en-us/azure/azure-relay/relay-what-is-it">Azure Relay</a> (but that requires calling it via HTTP from Power Automate, and it’s Service Bus behind the scenes anyways), so we decided to go with <a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/">Service Bus</a>.</p>

<p>Service Bus supports <a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-sessions">sessions</a> which can be used for implementing a <a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-sessions#request-response-pattern">request and response pattern</a>.</p>

<p>The implementation is really simple. Everything starts when a Power Automate gets triggered with some data payload (<a href="https://hajekj.net/2025/05/08/dynamically-executing-power-automate-flows-from-client/">from frontend</a> in our case). Next, we generate the session’s ID by using expression <a href="https://learn.microsoft.com/en-us/azure/logic-apps/expression-functions-reference#guid"><code class="language-plaintext highlighter-rouge">guid()</code></a> - we persist it in a variable, because we will also need it later.</p>

<p>In Service Bus, we <a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-quickstart-topics-subscriptions-portal#create-a-topic-using-the-azure-portal">create two topics</a> - <code class="language-plaintext highlighter-rouge">request</code> with on-premise <a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-quickstart-topics-subscriptions-portal#create-subscriptions-to-the-topic">subscriber</a> and <code class="language-plaintext highlighter-rouge">response</code> with Power Automate <a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-quickstart-topics-subscriptions-portal#create-subscriptions-to-the-topic">subscriber</a> (make sure to enable sessions).</p>

<p>Next we <a href="https://learn.microsoft.com/en-us/connectors/servicebus/#send-message">send the message to the queue/topic</a> and fill <em>Session Id</em> with the ID generated earlier. We also provide the message’s payload and any other things we need. This will send the message.</p>

<p>You then have a simple listener created via <a href="https://learn.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.servicebusclient.createprocessor?view=azure-dotnet"><code class="language-plaintext highlighter-rouge">CreateProcessor</code></a>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">DoWork</span><span class="p">(</span><span class="n">CancellationToken</span> <span class="n">stoppingToken</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">_serviceBusProcessor</span> <span class="p">=</span> <span class="n">_serviceBusClient</span><span class="p">.</span><span class="nf">CreateProcessor</span><span class="p">(</span><span class="s">"&lt;topic&gt;"</span><span class="p">,</span> <span class="s">"&lt;subscription&gt;"</span><span class="p">,</span> <span class="k">new</span> <span class="n">ServiceBusProcessorOptions</span>
    <span class="p">{</span>
        <span class="n">AutoCompleteMessages</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
        <span class="n">MaxConcurrentCalls</span> <span class="p">=</span> <span class="m">1</span><span class="p">,</span>
        <span class="n">MaxAutoLockRenewalDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromMinutes</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
    <span class="p">});</span>
    <span class="n">_serviceBusProcessor</span><span class="p">.</span><span class="n">ProcessMessageAsync</span> <span class="p">+=</span> <span class="n">MessageHandler</span><span class="p">;</span>
    <span class="n">_serviceBusProcessor</span><span class="p">.</span><span class="n">ProcessErrorAsync</span> <span class="p">+=</span> <span class="n">ErrorHandler</span><span class="p">;</span>

    <span class="k">await</span> <span class="n">_serviceBusProcessor</span><span class="p">.</span><span class="nf">StartProcessingAsync</span><span class="p">(</span><span class="n">stoppingToken</span><span class="p">);</span>

    <span class="k">while</span> <span class="p">(!</span><span class="n">stoppingToken</span><span class="p">.</span><span class="n">IsCancellationRequested</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Delay</span><span class="p">(</span><span class="m">10000</span><span class="p">,</span> <span class="n">stoppingToken</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And in the <code class="language-plaintext highlighter-rouge">MessageHandler</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">MessageHandler</span><span class="p">(</span><span class="n">ProcessMessageEventArgs</span> <span class="n">args</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">sessionId</span> <span class="p">=</span> <span class="n">args</span><span class="p">.</span><span class="n">Message</span><span class="p">.</span><span class="n">SessionId</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">message</span> <span class="p">=</span> <span class="n">args</span><span class="p">.</span><span class="n">Message</span><span class="p">.</span><span class="n">Body</span><span class="p">.</span><span class="n">ToObjectFromJson</span><span class="p">&lt;</span><span class="n">Message</span><span class="p">&gt;();</span>

    <span class="k">try</span>
    <span class="p">{</span>
        <span class="c1">// ... do your processing</span>
        <span class="k">await</span> <span class="n">_serviceBusSender</span><span class="p">.</span><span class="nf">SendMessageAsync</span><span class="p">(</span><span class="k">new</span> <span class="n">ServiceBusMessage</span>
        <span class="p">{</span>
            <span class="n">SessionId</span> <span class="p">=</span> <span class="n">sessionId</span><span class="p">,</span>
            <span class="n">ReplyToSessionId</span> <span class="p">=</span> <span class="n">sessionId</span><span class="p">,</span>
            <span class="n">Body</span> <span class="p">=</span> <span class="n">BinaryData</span><span class="p">.</span><span class="nf">FromObjectAsJson</span><span class="p">(</span><span class="k">new</span> <span class="n">Message</span>
            <span class="p">{</span>
                <span class="c1">// Your response data...</span>
            <span class="p">}),</span>
        <span class="p">});</span>
        <span class="k">await</span> <span class="n">args</span><span class="p">.</span><span class="nf">CompleteMessageAsync</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">Message</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="c1">// You should do proper error handling...</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">args</span><span class="p">.</span><span class="nf">DeadLetterMessageAsync</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">Message</span><span class="p">,</span> <span class="n">ex</span><span class="p">.</span><span class="n">Message</span><span class="p">);</span>
    <span class="p">}</span>

<span class="p">}</span>
</code></pre></div></div>

<p>Next in Power Automate, you will add a trigger into the flow’s body (yes, that’s right, you can use triggers mid-flow). In this case, we use <a href="https://learn.microsoft.com/en-us/connectors/servicebus/#when-a-message-is-received-in-a-topic-subscription-(peek-lock)"><em>When a message is received in a topic subscription (peek-lock)</em></a> trigger. The important thing is to provide the <em>Session id</em> parameter, so the flow will get triggered only on the reply to the initial message. When you receive the message, make sure to <a href="https://learn.microsoft.com/en-us/connectors/servicebus/#complete-the-message-in-a-queue">complete the message</a> since the trigger is only peek-lock (the <a href="https://learn.microsoft.com/en-us/connectors/servicebus/#when-a-message-is-received-in-a-topic-subscription-(auto-complete)">auto-complete</a> doesn’t support sessions).</p>

<p>This way, the flow pauses and just resumes running when a response arrives and thanks to it, you don’t need to manually store flow’s state and restore it.</p>

<p>If you are using HTTP Request trigger / Response action (or a connector based on these), remember the maximum execution time is <a href="https://learn.microsoft.com/en-us/power-automate/limits-and-config#timeout">2 minutes</a>, so if the execution time is longer, make sure to enable the <a href="https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/asychronous-flow-pattern">asynchronous response pattern</a> and properly consume it on the client.</p>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Microsoft Power Platform" /><category term="Microsoft Azure" /><category term="Power Automate" /><category term="Service Bus" /><summary type="html"><![CDATA[When we need to communicate with on-premise environment, we usually utilize Service Bus. Up until now, most of the messages have been fire and forget, however recently, we had a situation where we needed to perform two way communication (basically request and response) to basically perform some calculation on top of on-prem data and return the data.]]></summary></entry><entry><title type="html">Calling TDS endpoint from plugins</title><link href="https://hajekj.net/2025/11/06/calling-tds-endpoint-from-plugins/" rel="alternate" type="text/html" title="Calling TDS endpoint from plugins" /><published>2025-11-06T11:45:00+01:00</published><updated>2025-11-06T11:45:00+01:00</updated><id>https://hajekj.net/2025/11/06/calling-tds-endpoint-from-plugins</id><content type="html" xml:base="https://hajekj.net/2025/11/06/calling-tds-endpoint-from-plugins/"><![CDATA[<p>In Dataverse, you can use the <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/dataverse-sql-query">Tabular Data Stream (TDS) endpoint</a> to perform advanced queries over your data. It then allows you to talk to Dataverse as if it was a SQL server (read-only operations). In this post, we’re going to look at how to do this from plugin in a more native way.</p>

<!-- more -->

<h2 id="why-would-you-want-to-use-tds-in-your-plugin">Why would you want to use TDS in your plugin?</h2>

<p>For example to perform advanced <a href="https://www.inogic.com/blog/2025/02/retrieving-dynamics-365-data-using-sql-in-plugin-tds-endpoint/">roll-up</a> like operations - to calculate things in real-time or handle the calculation more efficiently - because FetchXML is limited compared to SQL.</p>

<p>For example, you can perform a query like this:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">contact</span> <span class="o">=</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="n">lastname</span><span class="p">)</span> <span class="k">as</span> <span class="k">count</span> <span class="k">FROM</span> <span class="n">contact</span><span class="p">),</span> <span class="n">account</span> <span class="o">=</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="k">as</span> <span class="k">count</span> <span class="k">FROM</span> <span class="n">account</span><span class="p">)</span>
</code></pre></div></div>

<p>Which will return a real-time count of the records (yes, even if it’s above 50,000), do multiple nested <code class="language-plaintext highlighter-rouge">GROUP BY</code> statements and much more.</p>

<h2 id="your-options">Your options</h2>

<h3 id="connecting-directly-via-sql">Connecting directly via SQL</h3>

<p>Probably the easiest option is to connect to the SQL directly from plugin which has been described <a href="https://www.inogic.com/blog/2025/02/retrieving-dynamics-365-data-using-sql-in-plugin-tds-endpoint/">here</a> or <a href="https://temmyraharjo.wordpress.com/2024/10/12/dataverse-retrieve-data-using-tds-endpoint-in-plugin/">here</a> for example. The issue with the code here, is that you also need to create an app registration and the query will then be authorized by the service principal’s permissions, rather than the user’s, which potentially introduces security risks. And there is no way to retrieve user’s token in the plugin to perform proper impersonation.</p>

<h3 id="using-executepowerbisql-request-from-within-plugin">Using <code class="language-plaintext highlighter-rouge">ExecutePowerBISql</code> request from within plugin</h3>

<p>Previously “documented” by Mark Carrington (<a href="https://markcarrington.dev/2020/08/04/msdyn365-internals-t-sql-endpoint/">here</a> and <a href="https://markcarrington.dev/2020/05/19/cds-t-sql-endpoint-pt-7-extensibility/">here</a>), you can make use of the <code class="language-plaintext highlighter-rouge">ExecutePowerBISql</code> message which allows you to pass in SQL query and retrieve the response. This message can be executed only through the <code class="language-plaintext highlighter-rouge">IOrganizationService</code> interface (calling it via <code class="language-plaintext highlighter-rouge">Xrm.WebApi.online.execute</code> won’t work) and it can be called from within a plugin which wasn’t previously possible (we created our own proxy in Azure Functions to be able to call TDS from client/server/Power Automate).</p>

<p>So from within your plugin, you can call code like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">request</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrganizationRequest</span><span class="p">(</span><span class="s">"ExecutePowerBISql"</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">Parameters</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ParameterCollection</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="s">"QueryText"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"select * from account"</span>
        <span class="c1">// ["NameMappingOptions"] = SqlNameMappingOptions.LogicalName  // Optional: https://learn.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.sqlnamemappingoptions?view=dataverse-sdk-latest</span>
        <span class="c1">// ["QueryParameters"] =  new ParameterCollection // Optional: https://learn.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.tdspowerbisqlrequest?view=dataverse-sdk-latest</span>
        <span class="c1">// {</span>
        <span class="c1">//     { "@parameter1", "Value1" },</span>
        <span class="c1">//     { "@parameter2", 123 },</span>
        <span class="c1">// }</span>
    <span class="p">}</span>
<span class="p">};</span>
<span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="n">localPluginContext</span><span class="p">.</span><span class="n">PluginUserService</span><span class="p">.</span><span class="nf">Execute</span><span class="p">(</span><span class="n">request</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">dataset</span> <span class="p">=</span> <span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Data</span><span class="p">.</span><span class="n">DataSet</span><span class="p">)</span><span class="n">response</span><span class="p">.</span><span class="n">Results</span><span class="p">[</span><span class="s">"Records"</span><span class="p">];</span>
<span class="c1">// ...</span>
</code></pre></div></div>

<p>You don’t need to perform any other authentication or create an app registration because it will use the calling user for authorization automatically, so it is much more convenient.</p>

<p>This however <strong>only works in a synchronous plugin</strong>, if you set it to run as asynchronous, you will end up with a missing dependency exception:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>System.ServiceModel.FaultException`1[Microsoft.Xrm.Sdk.OrganizationServiceFault]: An unexpected error occurred. (Fault Detail is equal to Exception details: 
ErrorCode: 0x80040216
Message: An unexpected error occurred.
TimeStamp: 2025-11-05T08:02:47.0000000Z
OriginalException: System.ServiceModel.FaultException`1[Microsoft.Xrm.Sdk.OrganizationServiceFault]: An unexpected error occurred. (Fault Detail is equal to Exception details: 
ErrorCode: 0x80040216
Message: An unexpected error occurred.
TimeStamp: 2025-11-05T08:02:47.6476346Z
--
Exception details: 
ErrorCode: 0x80040216
Message: System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.SqlServer.TransactSql.ScriptDom, Version=16.1.0.0, Culture=neutral, PublicKeyToken=[REDACTED] or one of its dependencies. The system cannot find the file specified.
at Microsoft.Crm.ObjectModel.PSqlService.GetSqlQueryEvaluationVisitor(String queryText, IExecutionContext executionContext, PSqlDatabaseContext pSqlDatabaseContext)
at Microsoft.Crm.ObjectModel.PSqlService.GetSqlExecutor(IExecutionContext executionContext, String queryText, PSqlDatabaseContext pSqlDatabaseContext...).
</code></pre></div></div>

<p><strong>UPDATE (11NOV2025):</strong> You can also encounter this error if you are executing this on Retrieve for example, and the retrieve operation is triggered from an asynchronous context (for example Dataverse trigger’s filter in Power Automate), because it will cause the code to run in the asynchronous sandbox.</p>

<h3 id="calling-from-client">Calling from client</h3>

<p>If you want to call this from the client (eg. client-script or PCF) the easiest option is to wrap the above into a <a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/custom-api">Custom API</a>. This will also allow you to call it from Power Automate.</p>

<p>Alternatively, you can call the organization service web proxy through JavaScript (<code class="language-plaintext highlighter-rouge">ExecutePowerBISql</code> is not available through OData endpoints unfortunately, so you have to use the SOAP call below):</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nf">executePowerBISql</span><span class="p">(</span><span class="nx">queryText</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// You can provide additional parameters as shown above</span>
    <span class="kd">const</span> <span class="nx">soapEnvelope</span> <span class="o">=</span> <span class="s2">`
    &lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;
      &lt;s:Body&gt;
        &lt;Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services"&gt;
          &lt;request i:type="a:OrganizationRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"&gt;
            &lt;a:Parameters xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic"&gt;
              &lt;a:KeyValuePairOfstringanyType&gt;
                &lt;b:key&gt;QueryText&lt;/b:key&gt;
                &lt;b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema"&gt;</span><span class="p">${</span><span class="nx">queryText</span><span class="p">}</span><span class="s2">&lt;/b:value&gt;
              &lt;/a:KeyValuePairOfstringanyType&gt;
            &lt;/a:Parameters&gt;
            &lt;a:RequestName&gt;ExecutePowerBISql&lt;/a:RequestName&gt;
          &lt;/request&gt;
        &lt;/Execute&gt;
      &lt;/s:Body&gt;
    &lt;/s:Envelope&gt;`</span><span class="p">;</span>

    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/XRMServices/2011/Organization.svc/web</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/xml; charset=utf-8</span><span class="dl">"</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">SOAPAction</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute</span><span class="dl">"</span>
        <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">soapEnvelope</span>
    <span class="p">});</span>

    <span class="kd">const</span> <span class="nx">responseText</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">text</span><span class="p">();</span>
    <span class="k">return</span> <span class="nx">responseText</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">executePowerBISql</span><span class="p">(</span><span class="dl">"</span><span class="s2">select * from account</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// parse result...</span>
</code></pre></div></div>

<h2 id="wrap-up">Wrap up</h2>

<p>You can see that calling TDS is possible from both client and plugin without any additional authentication requirements. While it’s probably not officially supported, it is quite a nice way to quickly perform advanced queries. I am not really happy that these things are not exposed to Power Platform developers because it just creates more complications when trying to do something a little more advanced (but it is what it is).</p>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Microsoft Power Platform" /><category term="Dataverse" /><summary type="html"><![CDATA[In Dataverse, you can use the Tabular Data Stream (TDS) endpoint to perform advanced queries over your data. It then allows you to talk to Dataverse as if it was a SQL server (read-only operations). In this post, we’re going to look at how to do this from plugin in a more native way.]]></summary></entry><entry><title type="html">Executing custom scripts from PCF</title><link href="https://hajekj.net/2025/10/06/executing-custom-scripts-from-pcf/" rel="alternate" type="text/html" title="Executing custom scripts from PCF" /><published>2025-10-06T20:00:00+02:00</published><updated>2025-10-06T20:00:00+02:00</updated><id>https://hajekj.net/2025/10/06/executing-custom-scripts-from-pcf</id><content type="html" xml:base="https://hajekj.net/2025/10/06/executing-custom-scripts-from-pcf/"><![CDATA[<p>We have hit some cases where we want to enable client extensibility of controls we develop - for example, display a custom dialog to enter metadata before a file is uploaded or to call <a href="https://hajekj.net/2025/04/28/using-entra-authentication-in-power-apps-pcfs-and-client-scripts/">token broker</a> to retrieve tokens. This can be achieved either through <a href="https://learn.microsoft.com/en-us/power-apps/developer/component-framework/events">events in PCF (preview)</a> or via calling into a custom script.</p>

<!-- more -->

<p>Before events were available, we utilized a library loaded on the form’s load event, and then executed it by finding the right iframe where the script lives and then executing the method passed in through parameters. In some cases, there are multiple “handlers” with specified order.</p>

<p>With events, obtaining a result through an async method becomes much more complicated, since you have to <a href="https://learn.microsoft.com/en-us/power-apps/developer/component-framework/tutorial-define-event?tabs=after#pass-payload-with-event">pass a custom method</a> to call back with result parameters and wait for the callback to be executed. Something like this:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">outputParameter1</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">promiseResolve</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">promise</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">promiseResolve</span> <span class="o">=</span> <span class="nx">resolve</span><span class="p">;</span>
<span class="p">});</span>
<span class="c1">// Wrap the rest into try {} catch (err) {}</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nf">customEvent1</span><span class="p">({</span>
    <span class="na">parameter1</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&lt;parameter_1&gt;</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">parameter2</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">parameter</span><span class="p">:</span> <span class="mi">2</span>
    <span class="p">},</span>
    <span class="na">callback</span><span class="p">:</span> <span class="p">(</span><span class="na">outputParam1</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">outputParameter1</span> <span class="o">=</span> <span class="nx">outputParam1</span><span class="p">;</span>
        <span class="nf">promiseResolve</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">});</span>
<span class="c1">// Use Promise.race to implement timeout and prevent hangs</span>
<span class="k">await</span> <span class="nf">promise</span><span class="p">();</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">outputParameter1</span><span class="p">);</span>
</code></pre></div></div>

<p>In the handler script, you would end up doing something like this:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nx">Your</span><span class="p">.</span><span class="nx">Namespace</span> <span class="p">{</span>
    <span class="k">export</span> <span class="kd">class</span> <span class="nc">Class</span> <span class="p">{</span>
        <span class="k">static</span> <span class="k">async</span> <span class="nc">OnLoad</span><span class="p">(</span><span class="nx">executionContext</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">formContext</span> <span class="o">=</span> <span class="nx">executionContext</span><span class="p">.</span><span class="nf">getFormContext</span><span class="p">();</span>
            <span class="kd">const</span> <span class="nx">sampleControl1</span> <span class="o">=</span> <span class="nx">formContext</span><span class="p">.</span><span class="nf">getControl</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;control_name&gt;</span><span class="dl">"</span><span class="p">);</span>
            <span class="nx">sampleControl1</span><span class="p">.</span><span class="nf">addEventHandler</span><span class="p">(</span><span class="dl">"</span><span class="s2">customEvent1</span><span class="dl">"</span><span class="p">,</span> <span class="k">async </span><span class="p">(</span><span class="nx">params</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="nf">log</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">parameter1</span><span class="p">);</span>
                <span class="kd">const</span> <span class="nx">accounts</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Xrm</span><span class="p">.</span><span class="nx">WebApi</span><span class="p">.</span><span class="nf">retrieveMultiple</span><span class="p">(</span><span class="dl">"</span><span class="s2">account</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">?$top=1</span><span class="dl">"</span><span class="p">);</span>
                <span class="nx">params</span><span class="p">.</span><span class="nf">callback</span><span class="p">(</span><span class="nx">accounts</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">]);</span>
            <span class="p">});</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>You can see that the code above is quite complex, and becomes even more complex for proper error handling, timeouts to prevent hangs etc. I haven’t tested what happens when you have multiple event subscribers, but I believe that it would result in even more complex code.</p>

<p>Over time, I discovered a method called <code class="language-plaintext highlighter-rouge">Xrm.Utility.executeFunction</code> which is undocumented, but allows you to specify a web resource, method to call and parameters to pass. It also handles script’s dependencies, translations etc. As a result, you will get a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"><code class="language-plaintext highlighter-rouge">Promise</code></a> which you can await to get the result. And you don’t need to attach the script beforehand.</p>

<p>Calling it is super simple:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Wrap this into try { } catch (err) { }</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Xrm</span><span class="p">.</span><span class="nx">Utility</span><span class="p">.</span><span class="nf">executeFunction</span><span class="p">(</span><span class="dl">"</span><span class="s2">webresource.js</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Your.Namespace.Class.Method</span><span class="dl">"</span><span class="p">,</span> <span class="p">[</span><span class="dl">"</span><span class="s2">&lt;parameter_1&gt;</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">parameter</span><span class="p">:</span> <span class="mi">2</span> <span class="p">}]);</span>
</code></pre></div></div>

<p>Handling the call and returning a result is also easy:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nx">Your</span><span class="p">.</span><span class="nx">Namespace</span> <span class="p">{</span>
    <span class="k">export</span> <span class="kd">class</span> <span class="nc">Class</span> <span class="p">{</span>
        <span class="k">static</span> <span class="k">async</span> <span class="nc">Method</span><span class="p">(</span><span class="nx">parameter1</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">parameter2</span><span class="p">:</span> <span class="nx">object</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">parameter1</span><span class="p">);</span>
            <span class="kd">const</span> <span class="nx">accounts</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Xrm</span><span class="p">.</span><span class="nx">WebApi</span><span class="p">.</span><span class="nf">retrieveMultiple</span><span class="p">(</span><span class="dl">"</span><span class="s2">account</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">?$top=1</span><span class="dl">"</span><span class="p">);</span>
            <span class="k">return</span> <span class="nx">accounts</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">];</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The code above will execute the script, but there’s a catch. The limit for execution is 10 seconds, if the code runs longer, <code class="language-plaintext highlighter-rouge">executeFunction</code> will throw an error. So if you need to use this to collect some data from the user - for example via a dialog or have something long running, you will still need to resolve to a similar pattern with <code class="language-plaintext highlighter-rouge">Promise</code> and callbacks like with events.</p>

<p>Something like this for the caller (you can make a nice wrapper for this):</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">outputParameter1</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">promiseResolve</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">promiseReject</span><span class="p">:</span> <span class="p">(</span><span class="nx">reason</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">promise</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">promiseResolve</span> <span class="o">=</span> <span class="nx">resolve</span><span class="p">;</span>
    <span class="nx">promiseReject</span> <span class="o">=</span> <span class="nx">reject</span><span class="p">;</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Xrm</span><span class="p">.</span><span class="nx">Utility</span><span class="p">.</span><span class="nf">executeFunction</span><span class="p">(</span><span class="dl">"</span><span class="s2">webresource.js</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Your.Namespace.Class.Method</span><span class="dl">"</span><span class="p">,</span> <span class="p">[</span>
    <span class="dl">"</span><span class="s2">&lt;parameter_1&gt;</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span> <span class="na">parameter</span><span class="p">:</span> <span class="mi">2</span> <span class="p">},</span>
    <span class="p">(</span><span class="nx">outputParam1</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">outputParameter1</span> <span class="o">=</span> <span class="nx">outputParam1</span><span class="p">;</span>
        <span class="nf">promiseResolve</span><span class="p">();</span>
    <span class="p">},</span>
    <span class="p">(</span><span class="nx">error</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nf">promiseReject</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">]);</span>
<span class="k">try</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nf">promise</span><span class="p">();</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">outputParameter1</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And this for the handler:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nx">Your</span><span class="p">.</span><span class="nx">Namespace</span> <span class="p">{</span>
    <span class="k">export</span> <span class="kd">class</span> <span class="nc">Class</span> <span class="p">{</span>
        <span class="k">static</span> <span class="k">async</span> <span class="nc">Method</span><span class="p">(</span><span class="nx">parameter1</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">parameter2</span><span class="p">:</span> <span class="nx">object</span><span class="p">,</span> <span class="nx">callback</span><span class="p">:</span> <span class="p">(</span><span class="nx">outputParam1</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">,</span> <span class="nx">reject</span><span class="p">:</span> <span class="p">(</span><span class="nx">error</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">boolean</span><span class="o">&gt;</span> <span class="p">{</span>
            <span class="p">(</span><span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="k">try</span> <span class="p">{</span>
                    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">parameter1</span><span class="p">);</span>
                    <span class="kd">const</span> <span class="nx">accounts</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Xrm</span><span class="p">.</span><span class="nx">WebApi</span><span class="p">.</span><span class="nf">retrieveMultiple</span><span class="p">(</span><span class="dl">"</span><span class="s2">account</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">?$top=1</span><span class="dl">"</span><span class="p">);</span>
                    <span class="nf">callback</span><span class="p">(</span><span class="nx">accounts</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">]);</span>
                <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
                    <span class="nf">reject</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">})();</span>
 
            <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This results in much cleaner and more readable code than using the wrappers for events. Have fun!</p>

<blockquote>
  <p>Remember that you can however only use <code class="language-plaintext highlighter-rouge">Xrm.*</code> methods in model-driven apps only, and it won’t work in canvas or Power Pages.</p>
</blockquote>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Microsoft Power Platform" /><category term="Power Apps component framework" /><summary type="html"><![CDATA[We have hit some cases where we want to enable client extensibility of controls we develop - for example, display a custom dialog to enter metadata before a file is uploaded or to call token broker to retrieve tokens. This can be achieved either through events in PCF (preview) or via calling into a custom script.]]></summary></entry><entry><title type="html">Speeding up PCF build with esbuild</title><link href="https://hajekj.net/2025/10/05/speeding-up-pcf-build-with-esbuild/" rel="alternate" type="text/html" title="Speeding up PCF build with esbuild" /><published>2025-10-05T10:15:00+02:00</published><updated>2025-10-05T10:15:00+02:00</updated><id>https://hajekj.net/2025/10/05/speeding-up-pcf-build-with-esbuild</id><content type="html" xml:base="https://hajekj.net/2025/10/05/speeding-up-pcf-build-with-esbuild/"><![CDATA[<p>Previously I wrote about <a href="https://hajekj.net/2025/03/01/speeding-up-pcf-build/">optimizations/fixes</a> which you can do to improve PCF build times with the default PCF setup. Since then, Microsoft has added a support for <a href="https://esbuild.github.io/">esbuild</a> build to <a href="https://www.npmjs.com/package/pcf-scripts">pcf-scripts</a> to provide an alternative to <a href="https://webpack.js.org/">webpack</a>.</p>

<!-- more -->

<p>Using esbuild to build PCF is however undocumented besides the release notes saying it’s an experimental feature and that it has to be enabled via <code class="language-plaintext highlighter-rouge">featureconfig.json</code> file (we will explore some other options in another article).</p>

<p>So when you create <code class="language-plaintext highlighter-rouge">featureconfig.json</code> file and add the following flag into it:</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">"pcfUseESBuild"</span><span class="p">:</span><span class="w"> </span><span class="s2">"on"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>You will also need to add a peer dependency called <code class="language-plaintext highlighter-rouge">esbuild</code> into your control. Simply do it by <code class="language-plaintext highlighter-rouge">npm i esbuild</code> or whatever you use as your package manager.</p>

<p>Once you set this up, you can now call <code class="language-plaintext highlighter-rouge">npm run build</code>. Hopefully your control will build without any issues (I haven’t hit any issues in our ~60 control repo - React, Fluent and bunch of other libraries used). Also remember that if it builds, it doesn’t mean it will work, so test it thoroughly.</p>

<p>The build will output <code class="language-plaintext highlighter-rouge">bundle.js</code>, you can even pack it into a solution, but if you run the control (via harness or in Power Apps), it will not run. The issue here is that this feature is either unfinished (yes, it’s experimental, but …) and probably lacks documentation, or more work needs to be done.</p>

<p>The control is missing registration with Power Apps host (or test harness host). This is a few lines of code which is added by a build step of webpack and looks like this:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if </span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">ComponentFramework</span> <span class="o">&amp;&amp;</span> <span class="nb">window</span><span class="p">.</span><span class="nx">ComponentFramework</span><span class="p">.</span><span class="nx">registerControl</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">ComponentFramework</span><span class="p">.</span><span class="nf">registerControl</span><span class="p">(</span><span class="dl">'</span><span class="s1">TALXIS.PCF.CompanyProfileHinting</span><span class="dl">'</span><span class="p">,</span> <span class="nx">pcf_tools_652ac3f36e1e4bca82eb3c1dc44e6fad</span><span class="p">.</span><span class="nx">CompanyProfileHinting</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">TALXIS</span> <span class="o">=</span> <span class="nx">TALXIS</span> <span class="o">||</span> <span class="p">{};</span>
    <span class="nx">TALXIS</span><span class="p">.</span><span class="nx">PCF</span> <span class="o">=</span> <span class="nx">TALXIS</span><span class="p">.</span><span class="nx">PCF</span> <span class="o">||</span> <span class="p">{};</span>
    <span class="nx">TALXIS</span><span class="p">.</span><span class="nx">PCF</span><span class="p">.</span><span class="nx">CompanyProfileHinting</span> <span class="o">=</span> <span class="nx">pcf_tools_652ac3f36e1e4bca82eb3c1dc44e6fad</span><span class="p">.</span><span class="nx">CompanyProfileHinting</span><span class="p">;</span>
    <span class="nx">pcf_tools_652ac3f36e1e4bca82eb3c1dc44e6fad</span> <span class="o">=</span> <span class="kc">undefined</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The most important part here is <code class="language-plaintext highlighter-rouge">ComponentFramework.registerControl</code> call, I haven’t found the use for the rest. So simply to end of your <code class="language-plaintext highlighter-rouge">index.ts</code> file, add the following:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @ts-ignore</span>
<span class="k">if </span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">ComponentFramework</span> <span class="o">&amp;&amp;</span> <span class="nb">window</span><span class="p">.</span><span class="nx">ComponentFramework</span><span class="p">.</span><span class="nx">registerControl</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// @ts-ignore</span>
    <span class="nx">ComponentFramework</span><span class="p">.</span><span class="nf">registerControl</span><span class="p">(</span><span class="dl">'</span><span class="s1">Your.Full.Namespace.Control</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Control</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">ComponentFramework.registerControl is not present!</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now try to build your control and run it either in Power Apps or control harness. <strong>It is going to build way faster</strong> than with webpack, and we’re talking tens of seconds saved with some large controls!</p>

<p>Are there any downsides to this? Yes. It is experimental, which means that it is not likely supported by Microsoft. It is undocumented and unfinished. It may change over time. It also doesn’t support any customization like webpack (<a href="https://learn.microsoft.com/en-us/power-apps/developer/component-framework/dependent-libraries#webpackconfigjs">custom webpack config</a>, <a href="https://learn.microsoft.com/en-us/power-apps/developer/component-framework/react-controls-platform-libraries">platform libraries</a> and more), but in our setup, we can do just fine without it. Also note, that <a href="https://esbuild.github.io/faq/#production-readiness">esbuild is still in development</a> and hasn’t hit 1.0 stable version at the time of writing.</p>

<p>I am really looking forward to see where Microsoft takes this (and I hope they allow some extensibility and add feature parity with webpack).</p>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Microsoft Power Platform" /><category term="Power Apps component framework" /><summary type="html"><![CDATA[Previously I wrote about optimizations/fixes which you can do to improve PCF build times with the default PCF setup. Since then, Microsoft has added a support for esbuild build to pcf-scripts to provide an alternative to webpack.]]></summary></entry><entry><title type="html">GitHub Organization Management with Entra ID</title><link href="https://hajekj.net/2025/09/21/github-organization-management-with-entra-id/" rel="alternate" type="text/html" title="GitHub Organization Management with Entra ID" /><published>2025-09-21T22:45:00+02:00</published><updated>2025-09-21T22:45:00+02:00</updated><id>https://hajekj.net/2025/09/21/github-organization-management-with-entra-id</id><content type="html" xml:base="https://hajekj.net/2025/09/21/github-organization-management-with-entra-id/"><![CDATA[<p>We are continuously trying to release some of our internal work as open source in NETWORG. One of the issues with doing so on GitHub however, is the management of users. Since we heavily rely on Azure DevOps internally, we are used to tight integration of Identity &amp; Access Management. GitHub however offers this only for <a href="https://github.com/pricing">enterprise plans</a>.</p>

<!-- more -->

<h1 id="github-enterprise-pricing">GitHub Enterprise pricing</h1>

<p>NETWORG is a Microsoft Partner and we get some of our licenses from <a href="https://learn.microsoft.com/en-us/partner-center/benefits/mpn-benefits-visual-studio">Microsoft Partner program</a> (Visual Studio Enterprise in this matter). Visual Studio Enterprise grants the user full access to Azure DevOps, but doesn’t grant any benefits in regards to GitHub (yes, there is some separate SKU, but you don’t get that). Additionally, a seat in Azure DevOps costs $6 per month per user, while with GitHub Enterprise, it is $21.</p>

<p>I don’t want to speculate on the topic “GitHub vs Azure DevOps and the future” so I will leave it out on purpose.</p>

<h1 id="why-on-earth-do-you-need-github-enterprise-when-doing-oss">Why on earth do you need GitHub Enterprise when doing OSS?</h1>

<p>Security and identity management. We streamlined our IAM into Entra ID. May it be for SSO enabled apps, <a href="https://hajekj.net/2023/09/18/entra-id-user-and-group-provisioning-with-bitwarden/">Bitwarden</a> or other apps. I want to have a single place to manage access, and especially onboarding and offboarding users.</p>

<p>Besides, we use GitHub for Open Source projects, our customer project are still in Azure DevOps, and things are going to stay that way.</p>

<p>I am aware there is <a href="https://about.gitlab.com/">GitLab</a>, and other solutions where you can setup SSO for free, but it’s not as convenient as using GitHub of course.</p>

<h1 id="you-want-security-you-have-to-pay-more">You want security, you have to pay more!</h1>

<p>This is not just limited to GitHub Enterprise, there are many software vendors who include SSO only in their Enterprise plans - which are literally unaffordable for small businesses. May it be <a href="https://www.twilio.com/en-us/editions">Twilio</a>, <a href="https://www.cloudflare.com/plans/">Cloudflare</a> and many others.</p>

<p>One of the things I am really having a hard time wrapping my head around - everybody is boasting about how secure their platforms are, yet, making every user own another account (and setting additional credentials) and adding administrative overhead (for onboarding and offboarding) probably doesn’t count, because in the end of the day, it’s on the shoulders of their customer, eg. us for example.</p>

<p>And I am not the only one having issue with <a href="https://www.reddit.com/r/msp/comments/i45cp7/why_is_it_sso_only_seems_available_on_expensive/">this</a>. There is even a site called <a href="https://sso.tax/">SSO.TAX</a> where they keep a list of vendors doing this practice.</p>

<blockquote>
  <p>I am well aware that things cost money, but I don’t want to pay for 50 other features which I will not use just to get SSO.</p>
</blockquote>

<h1 id="our-approach-and-solution">Our approach and solution</h1>

<p>Since we won’t have SSO with GitHub without getting Enterprise, and the costs for that are not really justifiable for doing just OSS and giving some bits back to the community, we decided to at least handle user provisioning and deprovisioning and team membership based on Entra information. This is where <strong><a href="https://github.com/NETWORG/github-organization-management">NETWORG/github-organization-management</a></strong> comes in.</p>

<h1 id="github-organization-management">GitHub Organization Management</h1>

<p>We decided to utilize GitHub’s rich <a href="https://docs.github.com/en/rest/orgs/members?apiVersion=2022-11-28#create-an-organization-invitation">APIs for organization management</a> to handle invites, removals and team memberships and synchronize it with Entra ID. This is not a first of its kind project - Microsoft already made and opensourced something similar - their <a href="https://github.com/microsoft/opensource-management-portal">Open Source Management Portal</a>. However their solution is a little bit overkill for our use - it handles roles, repo management, compliance and much - many of the things you don’t need when you are a small business.</p>

<p>Focusing on the basics we needed, it features the following:</p>

<ul>
  <li>Invite users to GitHub organizations</li>
  <li>Remove disabled/deleted/out-of-scope users from GitHub organizations</li>
  <li>Maintain team memberships based on Entra ID group memberships</li>
  <li>Supports B2B guest accounts and their membership in groups</li>
  <li>Doesn’t require GitHub Enterprise</li>
  <li>Doesn’t interfere with external collaborators</li>
  <li>Doesn’t interfere with direct assignments and teams not linked to Entra ID groups</li>
  <li>Supports multiple organizations against a single Entra ID tenant</li>
  <li>Exempt users (e.g. service accounts, admins) from organization removal (in case things go wrong)</li>
</ul>

<p>Through a simple user interface, users are required to link their Entra ID account with their GitHub account by signing in to both (GitHub account ID is persisted as a <a href="https://learn.microsoft.com/en-us/graph/api/resources/extensionproperty?view=graph-rest-1.0">directory schema extension</a> in Microsoft Graph), and then, based on the groups the person is member of, the access is calculated and applied - user is invited, and upon accepting invitation added and synchronized into GitHub Teams linked with Entra ID groups. When the account is disabled or deleted in Entra, the user will be removed from the organization (or when they lose access to the teams giving them the entitlement). GitHub Teams are linked based on an Entra Group ID provided in the team’s description field, eg. <code class="language-plaintext highlighter-rouge">Entra: &lt;group_id&gt;</code>.</p>

<p>This works perfectly fine with <a href="https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-overview">Entitlement Management</a> in Entra to manage memberships, even some basic Just-In-Time (JIT) access can be built thanks to this.</p>

<p>The user still has to use their regular GitHub account, but we <a href="https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/requiring-two-factor-authentication-in-your-organization">enforce use of 2FA at organization level</a> which in combination with the above is good enough.</p>

<p>Currently, you have to manually trigger the sync process via an HTTP endpoint, but this will be soon moved to a worker. We are currently running this project in <a href="https://learn.microsoft.com/en-us/azure/container-apps/dotnet-overview">Azure Container Apps</a>.</p>

<p>The project is open source and currently is still work in progress, but I just wanted to share the progress. You can follow it or even contribute to it on <strong><a href="https://github.com/NETWORG/github-organization-management">GitHub</a></strong>.</p>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Open Source" /><category term="GitHub" /><category term="Azure AD" /><category term="Entra ID" /><category term="SSO" /><summary type="html"><![CDATA[We are continuously trying to release some of our internal work as open source in NETWORG. One of the issues with doing so on GitHub however, is the management of users. Since we heavily rely on Azure DevOps internally, we are used to tight integration of Identity &amp; Access Management. GitHub however offers this only for enterprise plans.]]></summary></entry><entry><title type="html">Working with Exchange Online distribution groups via REST</title><link href="https://hajekj.net/2025/06/20/working-with-exchange-online-distribution-groups-via-rest/" rel="alternate" type="text/html" title="Working with Exchange Online distribution groups via REST" /><published>2025-06-20T10:00:00+02:00</published><updated>2025-06-20T10:00:00+02:00</updated><id>https://hajekj.net/2025/06/20/working-with-exchange-online-distribution-groups-via-rest</id><content type="html" xml:base="https://hajekj.net/2025/06/20/working-with-exchange-online-distribution-groups-via-rest/"><![CDATA[<p>For some time, we have been struggling with the way to programmatically manipulate distribution groups in Exchange Online. The only supported way is via <a href="https://learn.microsoft.com/en-us/powershell/exchange/connect-to-exchange-online-powershell?view=exchange-ps">Exchange Online PowerShell</a> which makes it quite hard to integrate into your code or execute from Power Automate. I dove a little bit deeper into how the module works and figured out a way to do this.</p>

<!-- more -->

<p>Distribution groups are somewhat a special object in Microsoft Graph. You can read them, but you can’t manipulate them (eg. add/remove members, rename etc.). This is because they are “Exchange mastered”, meaning that the group’s master data is stored in Exchange and only some part of it is replicated to Microsoft Graph, but there’s no functionality to replicate it back (despites Exchange support for <a href="https://techcommunity.microsoft.com/blog/exchange/exchange-online-improvements-to-accelerate-replication-of-changes-to-azure-activ/837218">dual-write</a>). I will not discuss whether you should be still using them or switching to M365 groups in this article.</p>

<p>So using <a href="https://www.telerik.com/fiddler">Fiddler</a> and executing <a href="https://learn.microsoft.com/en-us/powershell/module/exchange/new-distributiongroup?view=exchange-ps">New-DistributionGroup</a>, I discovered that the Exchange Online PowerShell calls the following REST API endpoint: <code class="language-plaintext highlighter-rouge">https://outlook.office365.com/adminapi/beta/&lt;tenantId&gt;/InvokeCommand</code> with the documented parameters. This allows them to execute <a href="https://learn.microsoft.com/en-us/powershell/module/exchange/?view=exchange-ps">any of the EXO PowerShell commands</a> and the API acts just like a PowerShell proxy.</p>

<p>So now I had to figure out authentication. I want to use the <a href="https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow">client_credentials flow</a>, since in my case the service is operating by a daemon, however you can use delegated permissions in case the user is interacting with your app or <a href="https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-powershell">Managed Identity</a> if you are running in Azure (or Azure Arc). We start by creating a standard <a href="https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app">app registration</a> (either single or multi-tenant) or you can make use of <a href="https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview">managed identity</a>. Then you need to go to <em>API Permissions</em> and add <em>Office 365 Exchange Online</em> app with application scope <code class="language-plaintext highlighter-rouge">Exchange.ManageAsApp</code> or simply add the following to your <a href="https://learn.microsoft.com/en-us/graph/api/resources/requiredresourceaccess?view=graph-rest-1.0">manifest’s <code class="language-plaintext highlighter-rouge">requiredResourceAccess</code></a>:</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">"resourceAppId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"00000002-0000-0ff1-ce00-000000000000"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"resourceAccess"</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">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dc50a0fb-09a3-484d-be87-e023b12c6440"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Role"</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>And then you need to give the application a role to manage Exchange. I <a href="https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal?tabs=admin-center">assigned</a> the application <a href="https://learn.microsoft.com/en-us/exchange/permissions-exo/permissions-exo#microsoft-365-permissions-in-exchange-online"><em>Exchange Recipient Administrator</em> role</a>.</p>

<p>The original setup was that we had a PowerShell Azure Function which used the <code class="language-plaintext highlighter-rouge">ExchangeOnlineManagement</code> module and executed the command, so the code below will be in PowerShell, but you can easily transform it to any language of your choice since those are just REST calls (or ask <a href="https://github.com/copilot">Copilot</a>).</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$clientId</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s2">"&lt;client_id&gt;"</span><span class="w">
</span><span class="nv">$clientSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">EXO_CLIENT_SECRET</span><span class="w"> </span><span class="c"># Your client_secret - retrieve it from ENV or Key Vault or somewhere</span><span class="w">
</span><span class="nv">$tenantId</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s2">"&lt;tenant&gt;.onmicrosoft.com"</span><span class="w">

</span><span class="nv">$tokenBody</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">client_id</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nv">$clientId</span><span class="w">
    </span><span class="nx">client_secret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$clientSecret</span><span class="w">
    </span><span class="nx">scope</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://outlook.office365.com/.default"</span><span class="w">
    </span><span class="nx">grant_type</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s2">"client_credentials"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$tokenResponse</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">Post</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"https://login.microsoftonline.com/</span><span class="nv">$tenantId</span><span class="s2">/oauth2/v2.0/token"</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="nv">$tokenBody</span><span class="w"> </span><span class="nt">-ContentType</span><span class="w"> </span><span class="s2">"application/x-www-form-urlencoded"</span><span class="w">
    </span><span class="nv">$accessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tokenResponse</span><span class="o">.</span><span class="nf">access_token</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c"># Handle token retrieval failure (this code applies to Azure Function)</span><span class="w">
    </span><span class="n">Push-OutputBinding</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">Response</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="p">([</span><span class="n">HttpResponseContext</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">StatusCode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">HttpStatusCode</span><span class="p">]</span><span class="err">::</span><span class="nx">Unauthorized</span><span class="w">
        </span><span class="nx">Body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Failed to acquire token: </span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Exception</span><span class="o">.</span><span class="nf">Message</span><span class="si">)</span><span class="s2">"</span><span class="w">
    </span><span class="p">})</span><span class="w">
    </span><span class="kr">return</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>So with this token, you can then execute the <code class="language-plaintext highlighter-rouge">InvokeCommand</code> endpoint as follows:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$groupOwner</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"&lt;alias&gt;@&lt;domain&gt;"</span><span class="w">
</span><span class="nv">$groupName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"&lt;displayName&gt;"</span><span class="w">
</span><span class="nv">$emailAddress</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"&lt;alias&gt;@&lt;domain&gt;"</span><span class="w">
</span><span class="nv">$members</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"&lt;alias1&gt;@&lt;domain&gt;"</span><span class="p">,</span><span class="w"> </span><span class="s2">"&lt;alias2&gt;@&lt;domain&gt;"</span><span class="p">)</span><span class="w">
</span><span class="nv">$anchorUpn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"&lt;alias&gt;@&lt;domain&gt;"</span><span class="w"> </span><span class="c"># This should be a mailbox in the same GEO as the DG you are targeting (for single geo, just use admin user's UPN)</span><span class="w">

</span><span class="nv">$payload</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">CmdletInput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
        </span><span class="nx">CmdletName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"New-DistributionGroup"</span><span class="w">
        </span><span class="nx">Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
            </span><span class="nx">RequireSenderAuthenticationEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
            </span><span class="nx">MemberJoinRestriction</span><span class="w">              </span><span class="o">=</span><span class="w"> </span><span class="s2">"Closed"</span><span class="w">
            </span><span class="nx">Members</span><span class="w">                            </span><span class="o">=</span><span class="w"> </span><span class="nv">$members</span><span class="w">
            </span><span class="nx">ManagedBy</span><span class="w">                          </span><span class="o">=</span><span class="w"> </span><span class="nv">$groupOwner</span><span class="w">
            </span><span class="nx">ErrorAction</span><span class="w">                        </span><span class="o">=</span><span class="w"> </span><span class="s2">"Stop"</span><span class="w">
            </span><span class="nx">MemberDepartRestriction</span><span class="w">            </span><span class="o">=</span><span class="w"> </span><span class="s2">"Closed"</span><span class="w">
            </span><span class="nx">Name</span><span class="w">                               </span><span class="o">=</span><span class="w"> </span><span class="nv">$displayName</span><span class="w">
            </span><span class="nx">PrimarySmtpAddress</span><span class="w">                 </span><span class="o">=</span><span class="w"> </span><span class="nv">$emailAddress</span><span class="w">
            </span><span class="nx">DisplayName</span><span class="w">                        </span><span class="o">=</span><span class="w"> </span><span class="nv">$displayName</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="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w">

</span><span class="nv">$headers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="s2">"Authorization"</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bearer </span><span class="nv">$accessToken</span><span class="s2">"</span><span class="w">
    </span><span class="s2">"X-CmdletName"</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="s2">"New-DistributionGroup"</span><span class="w">
    </span><span class="s2">"X-ResponseFormat"</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s2">"json"</span><span class="w"> </span><span class="c"># ExchangeOnlineManagement sends `clixml` but you can use `json` to have less hassle with the output</span><span class="w">
    </span><span class="s2">"X-ClientApplication"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"ExoManagementModule"</span><span class="w">
    </span><span class="s2">"X-AnchorMailbox"</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nv">$anchorUpn</span><span class="w">
    </span><span class="s2">"Content-Type"</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
    </span><span class="s2">"Accept"</span><span class="w">              </span><span class="o">=</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$webRequest</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-WebRequest</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"https://outlook.office365.com/adminapi/beta/</span><span class="nv">$tenantId</span><span class="s2">/InvokeCommand"</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">POST</span><span class="w"> </span><span class="nt">-Headers</span><span class="w"> </span><span class="nv">$headers</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="nv">$payload</span><span class="w"> </span><span class="nt">-ContentType</span><span class="w"> </span><span class="s1">'application/json'</span><span class="w"> </span><span class="nt">-UseBasicParsing</span><span class="w">
    </span><span class="nv">$responseJson</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$webRequest</span><span class="o">.</span><span class="nf">Content</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w">

    </span><span class="c"># Success, return the command response to the caller as JSON</span><span class="w">
    </span><span class="n">Push-OutputBinding</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">Response</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="p">([</span><span class="n">HttpResponseContext</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">StatusCode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">HttpStatusCode</span><span class="p">]</span><span class="err">::</span><span class="nx">OK</span><span class="w">
        </span><span class="nx">Body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$responseJson</span><span class="err">.</span><span class="nx">value</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w">
    </span><span class="p">})</span><span class="w">
    </span><span class="kr">return</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c"># Handle the exception</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This way, you can execute any supported command against EXO without the need of PowerShell and you can call it from Power Automate for example via HTTP actions.</p>

<p>Is this supported? I would say yes and no. Cince the <code class="language-plaintext highlighter-rouge">ExchangeOnlineManagement</code> module makes use of it (and the module itself IS supported), it shouldn’t matter whether you call these endpoints via the PowerShell module or via REST (since that’s what the module does anyways). If it breaks, just open Fiddler and adjust the commands accordingly (or use it in case you can’t figure out the correct parameters).</p>]]></content><author><name>Jan Hajek</name></author><category term="Microsoft" /><category term="Office 365" /><category term="PowerShell" /><category term="Microsoft Graph" /><category term="Exchange Online" /><summary type="html"><![CDATA[For some time, we have been struggling with the way to programmatically manipulate distribution groups in Exchange Online. The only supported way is via Exchange Online PowerShell which makes it quite hard to integrate into your code or execute from Power Automate. I dove a little bit deeper into how the module works and figured out a way to do this.]]></summary></entry></feed>