<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://christianbraeunlich.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://christianbraeunlich.com/" rel="alternate" type="text/html" /><updated>2024-08-31T11:39:26+00:00</updated><id>https://christianbraeunlich.com/feed.xml</id><title type="html">christianbraeunlich.com</title><subtitle>Blog by Christian Bräunlich</subtitle><author><name>Christian Bräunlich</name></author><entry><title type="html">Docker Engine VS Docker Desktop</title><link href="https://christianbraeunlich.com/docker/2024-08-31/dockerengine-vs-dockerdesktop/" rel="alternate" type="text/html" title="Docker Engine VS Docker Desktop" /><published>2024-08-31T00:00:00+00:00</published><updated>2024-08-31T00:00:00+00:00</updated><id>https://christianbraeunlich.com/docker/2024-08-31/dockerengine-vs-dockerdesktop</id><content type="html" xml:base="https://christianbraeunlich.com/docker/2024-08-31/dockerengine-vs-dockerdesktop/"><![CDATA[<p>When you start developing on your local Windows machine for (e.g. <em>Microsoft Dynamics 365 Business Central</em>), you will eventually encounter the topic of containerization with Docker. Probably the easiest and most common way among developers is to use Docker Desktop. It’s also recommended by Docker<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. With the update of the <em>Docker Desktop License Agreement</em><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> (effective date of these terms is August 31, 2021) you might consider using standalone Docker (Docker Engine) instead of Docker Desktop.</p>

<p class="notice--info">Commercial use of Docker Desktop at a company of more than 250 employees OR more than $10 million in annual revenue requires a paid subscription (Pro, Team, or Business).<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup></p>

<h1 id="windows-containers">Windows containers</h1>

<p>To install the Docker Engine, I use the script that is provided by the <em>microsoft/nav-arm-templates</em> repository on GitHub. It installs and updates Docker on your local windows machine and is mentioned on the <strong>Install Docker Engine</strong><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup> step in the <code class="language-plaintext highlighter-rouge">README.md</code> of the <em>navcontainerhelper</em> repository on GitHub.</p>

<pre><code class="language-pwsh">Invoke-WebRequest -UseBasicParsing -uri 'https://raw.githubusercontent.com/microsoft/nav-arm-templates/master/InstallOrUpdateDockerEngine.ps1' -OutFile (Join-Path $ENV:TEMP 'installorUpdateDocker.ps1')
. (Join-Path $ENV:TEMP 'installorUpdateDocker.ps1')
</code></pre>

<p>The <em>installorUpdateDocker</em> script will take care over numerous steps for you:</p>
<ol>
  <li>check if docker desktop is already installed</li>
  <li>install windows feature containers</li>
  <li>get latest stable version and URL</li>
  <li>check existing docker version</li>
  <li>download new version</li>
  <li>register service if necessary</li>
  <li>start the service</li>
</ol>

<p>Execute the <em>InstallOrUpdateDockerEngine.ps1</em> for the first time:</p>

<p align="center">
  <img alt="Execute the InstallOrUpdateDockerEngine.ps1 ends up in installing Docker Engine" src="/assets/images/posts/docker/2024-08-31/terminal-install.png" />
</p>

<p>Execute the <em>InstallOrUpdateDockerEngine.ps1</em> for the second time:</p>

<p align="center">
  <img alt="Execute the InstallOrUpdateDockerEngine.ps1 ends up in updating Docker Engine" src="/assets/images/posts/docker/2024-08-31/terminal-update.png" />
</p>

<h1 id="docker-engine-pros-and-cons">Docker Engine: Pros and Cons</h1>

<p>Docker has an official comparison: <a href="https://www.docker.com/wp-content/uploads/2022/03/Docker_Desktop_vs_DIY_datasheet_v2.pdf">Docker Desktop vs DIY with Docker Engine</a></p>

<p>Here are some of my own thoughts:</p>

<h2 id="pros">Pros</h2>

<ul>
  <li>It’s free to use (see LICENSE<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>)</li>
  <li>Production servers running docker engine, so it’s a good habit to familiarize yourself with docker commands</li>
  <li>If you only use the Docker commands, a GUI might be too much anyway</li>
</ul>

<h2 id="cons">Cons</h2>

<ul>
  <li>You do not get ready-to-use Linux and Windows containers, as would be the case with Docker Desktop</li>
  <li>You need to update the Docker Engine manually</li>
</ul>

<p align="center">
  <img alt="Docker Desktop Update Symbol Icon Triggered Meme" src="/assets/images/posts/docker/2024-08-31/2024-08-31-docker-desktop-update-triggered.png" />
</p>

<h1 id="linux-containers">Linux containers</h1>

<ul>
  <li>1. <a href="https://learn.microsoft.com/en-us/windows/wsl/install">Install WSL</a> (version 2) <code class="language-plaintext highlighter-rouge">wsl --install</code>
    <blockquote>
      <p>(by default, the installed Linux distribution will be Ubuntu. This can be changed by using the <code class="language-plaintext highlighter-rouge">-d</code> flag)</p>
    </blockquote>
    <ul>
      <li>1.1. Ubuntu has been installed. Changes will not be effective until the system is rebooted</li>
    </ul>

    <p align="center">
  <img alt="Execute the InstallOrUpdateDockerEngine.ps1 ends up in installing Docker Engine" src="/assets/images/posts/docker/2024-08-31/terminal-wsl-install.png" />
</p>

    <ul>
      <li>1.2. After reboot, a prompt will show up to setup Ubuntu (add a user)</li>
    </ul>

    <p align="center">
  <img alt="Execute the InstallOrUpdateDockerEngine.ps1 ends up in installing Docker Engine" src="/assets/images/posts/docker/2024-08-31/terminal-wsl-ubuntu-setup.png" />
</p>
  </li>
  <li>2. Open <a href="https://docs.docker.com/engine/install/">Install Docker Engine</a> of the Linux distribution you have chosen (we use Ubuntu)</li>
  <li>3. Choose an <a href="https://docs.docker.com/engine/install/ubuntu/#installation-methods">Installation Method</a>
    <ul>
      <li>
        <p>3.1. Set up Docker’s <strong>apt</strong> repository</p>

        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Add Docker's official GPG key:</span>
<span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install </span>ca-certificates curl
<span class="nb">sudo install</span> <span class="nt">-m</span> 0755 <span class="nt">-d</span> /etc/apt/keyrings
<span class="nb">sudo </span>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg <span class="nt">-o</span> /etc/apt/keyrings/docker.asc
<span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/docker.asc

<span class="c"># Add the repository to Apt sources:</span>
<span class="nb">echo</span> <span class="se">\</span>
  <span class="s2">"deb [arch=</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu </span><span class="se">\</span><span class="s2">
  </span><span class="si">$(</span><span class="nb">.</span> /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$VERSION_CODENAME</span><span class="s2">"</span><span class="si">)</span><span class="s2"> stable"</span> | <span class="se">\</span>
  <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null
<span class="nb">sudo </span>apt-get update
</code></pre></div>        </div>
      </li>
      <li>
        <p>3.2. Install the Docker packages</p>

        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nb">sudo </span>apt-get <span class="nb">install </span>docker-ce docker-ce-cli containerd.io
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
</ul>

<h1 id="conclusion">Conclusion</h1>

<p>Regardless of whether you use Docker Desktop or Docker Engine, you will end up creating Docker containers on your local machine. For Business Central development, we typically use scripts from the <strong>BcContainerHelper</strong> PowerShell module,  so we don’t miss the missing GUI too much. When managing multiple containers, a container management platform with a user interface can be particularly useful. In this case, Portainer is a great option, offering a web interface that makes it also accessible from other devices.</p>

<p>See you around 🦦</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://docs.docker.com/engine/install/binaries/#install-server-and-client-binaries-on-windows">https://docs.docker.com/engine/install/binaries/#install-server-and-client-binaries-on-windows</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://www.docker.com/blog/updating-product-subscriptions/">https://www.docker.com/blog/updating-product-subscriptions/</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://www.docker.com/pricing/">https://www.docker.com/pricing/</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://github.com/microsoft/navcontainerhelper?tab=readme-ov-file#1b-alternative-install-docker-engine-using-powershell">https://github.com/microsoft/navcontainerhelper?tab=readme-ov-file#1b-alternative-install-docker-engine-using-powershell</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p><a href="https://github.com/moby/moby/blob/master/LICENSE">https://github.com/moby/moby/blob/master/LICENSE</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlich</name></author><category term="Docker" /><category term="Container" /><category term="Docker" /><category term="Business Central" /><summary type="html"><![CDATA[When you start developing on your local Windows machine for (e.g. Microsoft Dynamics 365 Business Central), you will eventually encounter the topic of containerization with Docker. Probably the easiest and most common way among developers is to use Docker Desktop. It’s also recommended by Docker1. With the update of the Docker Desktop License Agreement2 (effective date of these terms is August 31, 2021) you might consider using standalone Docker (Docker Engine) instead of Docker Desktop. https://docs.docker.com/engine/install/binaries/#install-server-and-client-binaries-on-windows &#8617; https://www.docker.com/blog/updating-product-subscriptions/ &#8617;]]></summary></entry><entry><title type="html">Docker container BC v24 API timeout with large result set</title><link href="https://christianbraeunlich.com/businesscentral/2024-04-21/bc24-api-timeout/" rel="alternate" type="text/html" title="Docker container BC v24 API timeout with large result set" /><published>2024-04-21T00:00:00+00:00</published><updated>2024-04-21T00:00:00+00:00</updated><id>https://christianbraeunlich.com/businesscentral/2024-04-21/bc24-api-timeout</id><content type="html" xml:base="https://christianbraeunlich.com/businesscentral/2024-04-21/bc24-api-timeout/"><![CDATA[<p><strong>TL;DR</strong> if you create your bc v24 container in isolation mode hyperv and do not use SSL, you will not receive a response to API calls that return more than 50-60 result sets. You can avoid this issue by using SSL or using isolation mode process.</p>

<p>It’s been a while since my last blog post. As you may know, BC v24 has been released. I noticed that new BC APIs have been created. Woulnd’t be too much effort to add them to the Postman Collection<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>, …right? 😏 Well, this is where the fun begins! 😁</p>

<p>The first thing I’ve noticed is that I can not get a request through. I want to get the list of accounts. The request gets stuck. No answer. After 5 minutes, the request eventually times out.</p>

<p><img src="/assets/images/posts/msdyn365bc/2024-04-21-bc24-api-timeout/postman-request.png" alt="postman request" /></p>

<p>Ultimatley resulting in a bottleneck for all the otter requests as you can see in the event log of the container. The APIs which can usually be handled (&lt;50-60 results) have to wait.</p>

<p><img src="/assets/images/posts/msdyn365bc/2024-04-21-bc24-api-timeout/event-log.png" alt="event log" /></p>

<p>Detailed information can be found under <a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/operational-limits-online#ODataServicesUser">Operational limits</a>.</p>

<p>The picture in the header is the script I am using for the last couple of months. I am using BcContainerHelper v6.0.15. Never had a problem with the v23 APIs. But now suddenly they don’t work as expected in a BC v24 container, or at least some of them.</p>

<p>Surprisingly in the Business Central Launch Event<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> was mentioned that FindSet will be used for &gt;50 rows. This is roughly the same range as our timeout issue. Remains to be seen if this is related, as I have not taken the time to analyze the cause of this problem and hope that this is only a temporary issue and will be fixed in the future.</p>

<p><img src="/assets/images/posts/msdyn365bc/2024-04-21-bc24-api-timeout/youtube-bcle-faster-odata-data-loading.png" alt="youtube bcle faster odata data loading" /></p>

<p>Source: https://www.youtube.com/watch?v=VpNQ-CYNrkM&amp;list=PL1FESh9FqyhSjW63Ouft5vbc2NSxXKV2k&amp;index=39&amp;t=263s</p>

<p>Afaik, there are currently two solutions to this particular problem:</p>
<ul>
  <li>either stay with isolation mode hypver, but use ssl</li>
</ul>

<p>or</p>

<ul>
  <li>switch to isolation mode process</li>
</ul>

<p>Hope you learned something new.</p>

<p>See you around 🦦</p>

<h3 id="update">Update</h3>

<p>I’ve created a custom table with 3 fields (id, name and age) and inserted 50 records. Now insert 1 record step by step and send an http request in between. The GET requests run smoothly up to 58 records. If you increase the number by 1 more, it no longer responds and leads to a timeout.</p>

<p>To give you an example what this looks like, see the following animation.</p>

<p><img src="/assets/images/posts/msdyn365bc/2024-04-21-bc24-api-timeout/example.gif" alt="example" /></p>

<p>Adding one more field to the custom table and the custom api, the timeout now appears at 57 records, so it’s not just the number of records that matters, but also the size of the data.</p>

<p><img src="/assets/images/posts/msdyn365bc/2024-04-21-bc24-api-timeout/example2.gif" alt="example 2" /></p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://www.postman.com/christianbraeunlich/workspace/dynamics-365-business-central-api/collection/21650833-4d95f1ef-db20-4881-b2bd-5d546ff0566c">https://www.postman.com/christianbraeunlich/workspace/dynamics-365-business-central-api/collection/21650833-4d95f1ef-db20-4881-b2bd-5d546ff0566c</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://www.youtube.com/watch?v=FuY4EBBZf3c&amp;list=PL1FESh9FqyhSjW63Ouft5vbc2NSxXKV2k">https://www.youtube.com/watch?v=FuY4EBBZf3c&amp;list=PL1FESh9FqyhSjW63Ouft5vbc2NSxXKV2k</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlich</name></author><category term="BusinessCentral" /><category term="API" /><summary type="html"><![CDATA[The new docker containers of BC v24, with isolation mode hyperv and ssl disabled, have a problem with handling more than 50-60 data sets in the API.]]></summary></entry><entry><title type="html">Azure DevOps Pipeline Task: PR title validation</title><link href="https://christianbraeunlich.com/azure%20devops/2024-01-08/ado-validate-pr-title/" rel="alternate" type="text/html" title="Azure DevOps Pipeline Task: PR title validation" /><published>2024-01-08T00:00:00+00:00</published><updated>2024-01-08T00:00:00+00:00</updated><id>https://christianbraeunlich.com/azure%20devops/2024-01-08/ado-validate-pr-title</id><content type="html" xml:base="https://christianbraeunlich.com/azure%20devops/2024-01-08/ado-validate-pr-title/"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>When we think about bringing new developers into our project, it is likely that they will work with different guidelines than they have before. Sometimes the sheer amount of guidelines is simply too much to follow correctly after days, weeks or even months. Especially if you are not working entirely on the new project, but rather as a stand-in, meaning it could be weeks or months without working on this particular project, giving you a much harder time to keep-up with the specific guidelines.</p>

<p>One of the guidelines can be a prefix of the commit message by the project ticket number. This might not be that relevant if the pull request forces you to merge by <em>squash commit</em>. In my <a href="https://blog.christianbraeunlich.com/git/2023-12-25/git-hooks/">last post</a> I showed an example to <a href="https://blog.christianbraeunlich.com/git/2023-12-25/git-hooks/#commit-msg-validate-commit-message">validate the commit message</a>. However, since that is done locally there is always the possibility that some Devs might just turn the <em>Git hooks</em> off. That’s why we’d like to enforce rules centralized, and what better way than to automate this?</p>

<h1 id="getting-started">Getting started</h1>

<p>Let’s say we are forced to merge our pull requests via <em>squash commit</em>. This is already an enforced rule (“policy” in the likes of Azure DevOps) in the repository, usually applied to certain branches and tags.</p>

<p>Unfortunately there is no predefined variable for the pull request title in Azure DevOps yet. So we have to call the Azure DevOps API, get all the metadata of the specific pull request and essentially the title of the pull request, which we then validate by the defined Regex pattern, in this case: <code class="language-plaintext highlighter-rouge">[OTTER-xxxxxx] </code> while x stands for a number between 0 and 9. Also notice the whitespace after <strong>]</strong>.</p>

<h2 id="azure-devops-pipeline-task">Azure DevOps Pipeline Task</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Validate</span><span class="nv"> </span><span class="s">PR</span><span class="nv"> </span><span class="s">title'</span>
  <span class="na">env</span><span class="pi">:</span>
    <span class="na">SYSTEM_ACCESSTOKEN</span><span class="pi">:</span> <span class="s">$(System.AccessToken)</span>
    <span class="na">REGEX_VALIDATION</span><span class="pi">:</span> <span class="s">(\[OTTER\-[0-9]{1,6}\] \w+)</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">failOnStdErr</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">targetType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">inline'</span>
    <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">$url = "$($env:SYSTEM_COLLECTIONURI)_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/$($env:SYSTEM_PULLREQUEST_PULLREQUESTID)?api-version=4.0"</span>

      <span class="s">if(![string]::IsNullOrEmpty($env:SYSTEM_PULLREQUEST_PULLREQUESTID)) {</span>
        <span class="s">$pipeline = Invoke-RestMethod -Uri $url -Headers @{</span>
          <span class="s">"Authorization"="Bearer $($env:SYSTEM_ACCESSTOKEN)"</span>
        <span class="s">} -OutFile output.json</span>

        <span class="s">$Content = Get-Content output.json -Encoding UTF8</span>
        <span class="s">$jsonObj = $Content | ConvertFrom-Json</span>
        <span class="s">$pr_title = $jsonObj.title</span>

        <span class="s">$Regex = $env:REGEX_VALIDATION</span>
        <span class="s">if ($pr_title -match $Regex) {</span>
          <span class="s">Write-Host "The title of the pull request is valid."</span>
        <span class="s">} else {</span>
          <span class="s">Throw "Error: Invalid pull request title! Please prefix with a valid project ticket number."</span>
        <span class="s">}</span>
      <span class="s">}</span>
</code></pre></div></div>

<p>The script isn’t optimized. There is an error while reading the json content from the http response. This is why I opted in for the quick workaround, saving the content in a file and reading it again. The error handling can be improved. Also the task needs to be executed only if there is an actual PR, otherwise this script doesn’t make sense which is why I just let it pass. (Maybe I update this script in the future).</p>

<h1 id="conclusion">Conclusion</h1>

<p>These automations guide us through the various guidelines of each project/repository without having to read a lot of documentation and also help the teams managing the project to enforce their policies.</p>

<p>The developers get immediate feedback on their pull requests, which leads to fewer delays and more productivity. Now it’s your turn. Get creative and create your own tasks to enforce your rules.</p>

<p>Hope you learned something new.</p>

<p>See you around 🦦</p>]]></content><author><name>christianbraeunlich</name></author><category term="Azure DevOps" /><category term="Azure DevOps" /><category term="PowerShell" /><category term="Regex" /><summary type="html"><![CDATA[A PowerShell task to validate a prefixed project ticket number in the Azure DevOps pull request title.]]></summary></entry><entry><title type="html">How to use Git Hooks to automate development tasks?</title><link href="https://christianbraeunlich.com/git/2023-12-25/git-hooks/" rel="alternate" type="text/html" title="How to use Git Hooks to automate development tasks?" /><published>2023-12-25T00:00:00+00:00</published><updated>2023-12-25T00:00:00+00:00</updated><id>https://christianbraeunlich.com/git/2023-12-25/git-hooks</id><content type="html" xml:base="https://christianbraeunlich.com/git/2023-12-25/git-hooks/"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>If you frequently clone new repositories, it can be a challenge to adhere to the specific development policy. New project, new development team, new software development policy. This doesn’t sound familiar to you? Then perhaps one of the following:</p>
<ul>
  <li>“It would be cool if I could do X automatically befor pushing my Git commits.”</li>
  <li>“I still keep forgetting running my unit tests in between each Git commit.”</li>
  <li>“We have to prefix our Git commit message with the project ticket number.” (see <a href="#examples">Examples</a>)</li>
</ul>

<p>So.. Git hooks to the rescue! They allow us to customize the behavior of Git and trigger actions that are automatically executed when a specific Git event occurs. Development teams can enforce commit policies and implement CI workflows. You can use Git hooks to automate almost any aspect of your development workflow.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>I’ll cover only local hooks and not server-side hooks, and show two small examples and since the <code class="language-plaintext highlighter-rouge">.git</code> folder is always ignored, we can not share those scripts out-of-the-box within our development team, so let’s have a look at how we can share our Git hooks aswell.</p>

<h1 id="prerequisites">Prerequisites</h1>

<ul>
  <li>Git v2.9.0<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> as a minimum</li>
</ul>

<h1 id="getting-started">Getting started</h1>

<p>After executing the <code class="language-plaintext highlighter-rouge">git init</code> command, Git creates a new directory with the following structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>local repository
├── .git
|   ├── hooks
|   |   ├── commit-msg.sample
|   |   ├── pre-commit.sample
|   |   ├── prepare-commit-msg.sample
|   |   ├── pre-push.sample
|   |   └── ...
|   ├── ...
|   ├── config
|   ├── description
|   └── HEAD
├── .gitignore
└── README.md
</code></pre></div></div>

<p>One of the folders is called <em>hooks</em> and contains only files with the extension <code class="language-plaintext highlighter-rouge">.sample</code>. The comment section of these files states that these pre-defined <em>Git hooks</em> become active when the filename extension <code class="language-plaintext highlighter-rouge">.sample</code> is removed.</p>

<p>For a full list of all available Git hooks, see: <a href="https://git-scm.com/docs/githooks#_hooks">https://git-scm.com/docs/githooks#_hooks</a></p>

<p class="notice--info">Make sure that the Git hook is executable, otherwise it won’t run.</p>

<p>To change the script interpreter, you can change the first line of the file (<em>Shebang</em> <code class="language-plaintext highlighter-rouge">#!/bin/sh</code>) to your preferred scripting language.</p>

<h1 id="configuration">Configuration</h1>

<p>Since we cannot share the Git hooks from the <code class="language-plaintext highlighter-rouge">.git</code> directory,
we create a new folder with the name <code class="language-plaintext highlighter-rouge">.git-hooks</code> and put all <em>Git hooks</em> into this folder.</p>

<p>Advise your development team that they will need to change the default path to the new <em>Git hooks</em> folder for this to take effect. To change the path of the hooks for the current local repository, run the command:</p>

<p><code class="language-plaintext highlighter-rouge">git config core.hooksPath ./.git-hooks</code></p>

<p class="notice--info">It’s a good idea to add this step to the <em>Getting Started</em> / <em>Setup</em> section of your repository’s <code class="language-plaintext highlighter-rouge">README.md</code> file. This is a common place that every developer should read after cloning the repository.</p>

<p>If you want to take this one step further and copy <em>Git hooks</em> to the new repository you create/clone, see <em>Git Template</em>: <a href="https://git-scm.com/docs/git-init#_template_directory">https://git-scm.com/docs/git-init#_template_directory</a></p>

<h1 id="examples">Examples</h1>

<p>If you decide that it fits well into your development process to add the ticket number to every single commit message, perhaps if you decide not to squash commits after you have completed the pull request, it can become very tedious for developers to add the ticket number every time, especially for those who commit in small increments and therefore a lot.</p>

<p>Let’s take a look at two examples. One where we trigger a validation step and another where a prefix is added to the commit message. Both examples come from the boring execution of adding the ticket number as prefix to the commit message. Normally we already name the branch with the ticket number, so why bother and just add it automatically.</p>

<h2 id="commit-msg-validate-commit-message">commit-msg: Validate Commit Message</h2>

<p>If you do not name your branch after a specific ticket number, but still want to check the commit message prefixed with a ticket number, you can use the following script.</p>

<p>Content of the <code class="language-plaintext highlighter-rouge">commit-msg</code> file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>

<span class="nv">FILE_COMMIT_EDITMSG</span><span class="o">=</span><span class="nv">$1</span>
<span class="nv">COMMIT_MSG</span><span class="o">=</span><span class="sb">`</span><span class="nb">head</span> <span class="nt">-n</span> 1 <span class="nv">$FILE_COMMIT_EDITMSG</span><span class="sb">`</span>
<span class="nv">REGEX_RESULT</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$COMMIT_MSG</span> | <span class="nb">grep</span> <span class="nt">-Eo</span> <span class="s1">'(\[[A-Z]+-[0-9]+\]\s)'</span><span class="si">)</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nv">$REGEX_RESULT</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"Git commit message '</span><span class="nv">$COMMIT_MSG</span><span class="s2">' is missing a ticket number. Example '[OTTER-1234] add component'"</span>
  <span class="nb">exit </span>1
<span class="k">fi</span>
</code></pre></div></div>

<h2 id="prepare-commit-msg-prefix-commit-message">prepare-commit-msg: Prefix Commit Message</h2>

<p>Usually, the name of the branch already contains the ticket number. Therefore, we can automatically insert the ticket number into the commit message based on the name of the current branch.</p>

<p>Content of the <code class="language-plaintext highlighter-rouge">prepare-commit-msg</code> file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>

<span class="nv">FILE_COMMIT_EDITMSG</span><span class="o">=</span><span class="nv">$1</span>
<span class="nv">COMMIT_MSG</span><span class="o">=</span><span class="sb">`</span><span class="nb">head</span> <span class="nt">-n</span> 1 <span class="nv">$FILE_COMMIT_EDITMSG</span><span class="sb">`</span>
<span class="nv">BRANCH_NAME</span><span class="o">=</span><span class="sb">`</span>git rev-parse <span class="nt">--abbrev-ref</span> HEAD<span class="sb">`</span>
<span class="nv">REGEX_RESULT</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$BRANCH_NAME</span> | <span class="nb">grep</span> <span class="nt">-Eo</span> <span class="s1">'([A-Z][A-Z0-9]+-[0-9]+)'</span><span class="si">)</span>
<span class="k">if</span> <span class="o">[</span> <span class="nv">$REGEX_RESULT</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"The current branch name does not contain a valid ticket number. Example 'feature/OTTER-1234-new-feature'"</span>
  <span class="nb">exit </span>1<span class="p">;</span>
<span class="k">fi

</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$REGEX_RESULT</span><span class="s2"> </span><span class="nv">$COMMIT_MSG</span><span class="s2">"</span> <span class="o">&gt;</span> <span class="nv">$FILE_COMMIT_EDITMSG</span>
</code></pre></div></div>

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

<p>A few additional sources you can read to dive a bit deeper into the <em>Git hooks</em> topic:</p>

<ul>
  <li><a href="https://github.com/git/git/blob/master/Documentation/githooks.txt">https://github.com/git/git/blob/master/Documentation/githooks.txt</a></li>
  <li><a href="https://www.atlassian.com/de/git/tutorials/git-hooks">https://www.atlassian.com/de/git/tutorials/git-hooks</a></li>
  <li><a href="https://www.digitalocean.com/community/tutorials/how-to-use-git-hooks-to-automate-development-and-deployment-tasks">https://www.digitalocean.com/community/tutorials/how-to-use-git-hooks-to-automate-development-and-deployment-tasks</a></li>
  <li><a href="https://kinsta.com/blog/git-hooks/">https://kinsta.com/blog/git-hooks/</a></li>
</ul>

<h1 id="conclusion">Conclusion</h1>

<p>I hope these two practical examples will inspire you to get creative and create your own <em>Git hooks</em>. You can basically add every step you want, you are only limited by your skills and imagination. So.. automate the boring stuff and enforce local development policies! Please don’t bloat it too much to ensure fast and smooth development.</p>

<p>Hope you learned something new.</p>

<p>See you around 🦦</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://www.atlassian.com/git/tutorials/git-hooks">https://www.atlassian.com/git/tutorials/git-hooks</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://github.com/git/git/blob/master/Documentation/RelNotes/2.9.0.txt#L127-L128">https://github.com/git/git/blob/master/Documentation/RelNotes/2.9.0.txt#L127-L128</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlich</name></author><category term="Git" /><category term="Git" /><summary type="html"><![CDATA[Use Git Hooks in your local development environment to automate development tasks and enforce guidelines across your team.]]></summary></entry><entry><title type="html">Enhancing User Experience: The Power of Application Areas in System Development</title><link href="https://christianbraeunlich.com/businesscentral/2023-08-29/user-experience/" rel="alternate" type="text/html" title="Enhancing User Experience: The Power of Application Areas in System Development" /><published>2023-08-29T00:00:00+00:00</published><updated>2023-08-29T00:00:00+00:00</updated><id>https://christianbraeunlich.com/businesscentral/2023-08-29/user-experience</id><content type="html" xml:base="https://christianbraeunlich.com/businesscentral/2023-08-29/user-experience/"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Application Area gives us the ability to define differentiated user experiences. They are mapped to controls to show or hide them on page objects to enable more or fewer business scenarios.</p>

<p>As you know, we can extend the application areas in Business Central<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> by extending the <strong>Application Area Setup</strong> table:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tableextension</span> <span class="m">60200</span> <span class="n">ApplicationAreaSetupExt</span> <span class="n">extends</span> <span class="s">"Application Area Setup"</span>
<span class="p">{</span>
    <span class="n">fields</span>
    <span class="p">{</span>
        <span class="nf">field</span><span class="p">(</span><span class="m">60200</span><span class="p">;</span> <span class="s">"Trading"</span><span class="p">;</span> <span class="n">Boolean</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 class="notice--info">Spaces in the field name are omitted in the ApplicationArea property.</p>

<p>Then we can set the <em>ApplicationArea</em> property<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> to our newly created application area, as in the following pagefield example:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">field</span><span class="p">(</span><span class="n">TradingViewEnabled</span><span class="p">;</span> <span class="n">Rec</span><span class="p">.</span><span class="s">"Trading View Enabled"</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">ApplicationArea</span> <span class="p">=</span> <span class="n">Trading</span><span class="p">;</span>
    <span class="n">ToolTip</span> <span class="p">=</span> <span class="err">'</span><span class="n">Specifies</span> <span class="k">if</span> <span class="n">the</span> <span class="n">trading</span> <span class="n">view</span> <span class="k">is</span> <span class="n">enabled</span><span class="p">.</span><span class="err">'</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>However, we can not enable/disable an application area out of the box. This is where the <em>Experience Tier</em> comes into play.</p>

<h1 id="extend-the-experience-tier-setup">Extend the Experience Tier Setup</h1>

<p>By adding experience tiers we allow the users to choose which set of application areas they want to enable in their specific company. We can change the <strong>User Experience</strong> on the <strong>Company Information</strong> page.</p>

<p>The way it works is the same as extending the <em>Application Area Setup</em>, except this time it extends the <strong>Experience Tier Setup</strong>. Let’s look at an example:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tableextension</span> <span class="m">60201</span> <span class="n">ExperienceTierSetupExt</span> <span class="n">extends</span> <span class="s">"Experience Tier Setup"</span>
<span class="p">{</span>
    <span class="n">fields</span>
    <span class="p">{</span>
        <span class="nf">field</span><span class="p">(</span><span class="m">50100</span><span class="p">;</span> <span class="n">Ultimate</span><span class="p">;</span> <span class="n">Boolean</span><span class="p">)</span>
        <span class="p">{</span>
        <span class="p">}</span>
        <span class="nf">field</span><span class="p">(</span><span class="m">50101</span><span class="p">;</span> <span class="n">Platinum</span><span class="p">;</span> <span class="n">Boolean</span><span class="p">)</span>
        <span class="p">{</span>
        <span class="p">}</span>
        <span class="nf">field</span><span class="p">(</span><span class="m">50102</span><span class="p">;</span> <span class="n">VIP</span><span class="p">;</span> <span class="n">Boolean</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>

<h2 id="enable-application-areas">Enable application areas</h2>

<p>To enable application areas in a custom experience tier we need to subscribe to the event “OnSetExperienceTier” from the “Application Area Mgmt. Facade” codeunit.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">codeunit</span> <span class="m">60200</span> <span class="n">EnableApplicationArea</span>
<span class="p">{</span>
    <span class="n">Access</span> <span class="p">=</span> <span class="n">Internal</span><span class="p">;</span>
    <span class="n">InherentEntitlements</span> <span class="p">=</span> <span class="n">X</span><span class="p">;</span>
    <span class="n">InherentPermissions</span> <span class="p">=</span> <span class="n">X</span><span class="p">;</span>
    <span class="n">SingleInstance</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="n">Codeunit</span><span class="p">::</span><span class="s">"Application Area Mgmt. Facade"</span><span class="p">,</span> <span class="err">'</span><span class="n">OnSetExperienceTier</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">false</span><span class="p">)]</span>
    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">EnableAdvancedApplicationAreaOnSetExperienceTier</span><span class="p">(</span><span class="n">experienceTierSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Experience Tier Setup"</span><span class="p">;</span> <span class="kt">var</span> <span class="n">tempApplicationAreaSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Application Area Setup"</span> <span class="n">temporary</span><span class="p">;</span> <span class="kt">var</span> <span class="n">applicationAreasSet</span><span class="p">:</span> <span class="n">Boolean</span><span class="p">)</span>
    <span class="n">begin</span>
        <span class="k">if</span> <span class="n">experienceTierSetup</span><span class="p">.</span><span class="n">Ultimate</span> <span class="n">then</span> <span class="n">begin</span>
            <span class="n">tempApplicationAreaSetup</span><span class="p">.</span><span class="n">Basic</span> <span class="p">:=</span> <span class="k">true</span><span class="p">;</span>
            <span class="n">tempApplicationAreaSetup</span><span class="p">.</span><span class="n">Suite</span> <span class="p">:=</span> <span class="k">true</span><span class="p">;</span>
            <span class="n">tempApplicationAreaSetup</span><span class="p">.</span><span class="n">Service</span> <span class="p">:=</span> <span class="k">true</span><span class="p">;</span>
        <span class="n">end</span><span class="p">;</span>
    <span class="n">end</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For an overview, which application areas are enabled in the current company, search for <strong>Application Area</strong> via <em>Tell Me</em>:</p>

<p align="center">
  <img alt="Application Area" src="/assets/images/posts/msdyn365bc/2023-08-29-user-experience/2023-08-29-application-areas.png" />
</p>

<h2 id="refresh-experience-tier-of-current-company">Refresh experience tier of current company</h2>

<p>There is still one more thing left to do: set the experience tier of the current company. This is done when installing the extension as in the following example:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">codeunit</span> <span class="m">60201</span> <span class="n">InstallApplicationArea</span>
<span class="p">{</span>
    <span class="n">Subtype</span> <span class="p">=</span> <span class="n">Install</span><span class="p">;</span>

    <span class="n">trigger</span> <span class="nf">OnInstallAppPerCompany</span><span class="p">()</span>
    <span class="kt">var</span>
        <span class="n">applicationAreaMgmtFacade</span><span class="p">:</span> <span class="n">Codeunit</span> <span class="s">"Application Area Mgmt. Facade"</span><span class="p">;</span>
    <span class="n">begin</span>
        <span class="n">applicationAreaMgmtFacade</span><span class="p">.</span><span class="nf">RefreshExperienceTierCurrentCompany</span><span class="p">();</span>
    <span class="n">end</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you have already noticed, it’s necessary to update our AL code whenever there are new <em>Application Areas</em> added to our solution.</p>

<h1 id="enable-the-application-area-via-setup-page">Enable the Application Area via Setup page</h1>

<p>Since we can design module-driven in Business Central, I think that enabling the Application Area in a setup page makes sense. It gives the administrators the ability to enable/disable the module for specific companies without switchting the Experience Tier.</p>

<p>The following example <strong>Module Setup</strong> table just goes to show how this can be done. This time we refresh the experience tier on validating the <strong>Application Area Enabled</strong> field.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">table</span> <span class="m">60200</span> <span class="s">"Module Setup"</span>
<span class="p">{</span>
    <span class="n">Caption</span> <span class="p">=</span> <span class="err">'</span><span class="n">Module</span> <span class="n">Setup</span><span class="err">'</span><span class="p">;</span>

    <span class="n">fields</span>
    <span class="p">{</span>
        <span class="nf">field</span><span class="p">(</span><span class="m">1</span><span class="p">;</span> <span class="s">"Primary Key"</span><span class="p">;</span> <span class="n">Code</span><span class="p">[</span><span class="m">10</span><span class="p">])</span>
        <span class="p">{</span>
            <span class="n">Caption</span> <span class="p">=</span> <span class="err">'</span><span class="n">Primary</span> <span class="n">Key</span><span class="err">'</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="nf">field</span><span class="p">(</span><span class="m">10</span><span class="p">;</span> <span class="s">"Application Area Enabled"</span><span class="p">;</span> <span class="n">Boolean</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">Caption</span> <span class="p">=</span> <span class="err">'</span><span class="n">Application</span> <span class="n">Area</span> <span class="n">Enabled</span><span class="err">'</span><span class="p">;</span>

            <span class="n">trigger</span> <span class="nf">OnValidate</span><span class="p">()</span>
            <span class="kt">var</span>
                <span class="n">applicationAreaMgmtFacade</span><span class="p">:</span> <span class="n">Codeunit</span> <span class="s">"Application Area Mgmt. Facade"</span><span class="p">;</span>
            <span class="n">begin</span>
                <span class="n">applicationAreaMgmtFacade</span><span class="p">.</span><span class="nf">RefreshExperienceTierCurrentCompany</span><span class="p">();</span>
            <span class="n">end</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This triggers our event subscribers so that we can activate the application area depending on the value of the Boolean.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">moduleSetup</span><span class="p">.</span><span class="nf">Get</span><span class="p">()</span> <span class="n">then</span>
    <span class="n">tempApplicationAreaSetup</span><span class="p">.</span><span class="n">Trading</span> <span class="p">:=</span> <span class="n">moduleSetup</span><span class="p">.</span><span class="s">"Application Area Enabled"</span><span class="p">;</span>
</code></pre></div></div>

<p>To cover every aspect, here’s a full example:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">codeunit</span> <span class="m">50103</span> <span class="n">EnableApplicationArea</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="n">Codeunit</span><span class="p">::</span><span class="s">"Application Area Mgmt."</span><span class="p">,</span> <span class="err">'</span><span class="n">OnGetBasicExperienceAppAreas</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">false</span><span class="p">)]</span>
    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">EnableModuleOnGetBasicExperienceAppAreas</span><span class="p">(</span><span class="kt">var</span> <span class="n">tempApplicationAreaSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Application Area Setup"</span> <span class="n">temporary</span><span class="p">)</span>
    <span class="n">begin</span>
        <span class="nf">EnableApplicationArea</span><span class="p">(</span><span class="n">tempApplicationAreaSetup</span><span class="p">);</span>
    <span class="n">end</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="n">Codeunit</span><span class="p">::</span><span class="s">"Application Area Mgmt."</span><span class="p">,</span> <span class="err">'</span><span class="n">OnGetEssentialExperienceAppAreas</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">false</span><span class="p">)]</span>
    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">EnableModuleOnGetEssentialExperienceAppAreas</span><span class="p">(</span><span class="kt">var</span> <span class="n">tempApplicationAreaSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Application Area Setup"</span> <span class="n">temporary</span><span class="p">)</span>
    <span class="n">begin</span>
        <span class="nf">EnableApplicationArea</span><span class="p">(</span><span class="n">tempApplicationAreaSetup</span><span class="p">);</span>
    <span class="n">end</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="n">Codeunit</span><span class="p">::</span><span class="s">"Application Area Mgmt."</span><span class="p">,</span> <span class="err">'</span><span class="n">OnGetPremiumExperienceAppAreas</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">false</span><span class="p">)]</span>
    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">EnableOnGetPremiumExperienceAppAreas</span><span class="p">(</span><span class="kt">var</span> <span class="n">tempApplicationAreaSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Application Area Setup"</span> <span class="n">temporary</span><span class="p">)</span>
    <span class="n">begin</span>
        <span class="nf">EnableApplicationArea</span><span class="p">(</span><span class="n">tempApplicationAreaSetup</span><span class="p">);</span>
    <span class="n">end</span><span class="p">;</span>

    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">EnableApplicationArea</span><span class="p">(</span><span class="kt">var</span> <span class="n">tempApplicationAreaSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Application Area Setup"</span> <span class="n">temporary</span><span class="p">)</span>
    <span class="kt">var</span>
        <span class="n">moduleSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Module Setup"</span><span class="p">;</span>
    <span class="n">begin</span>
        <span class="k">if</span> <span class="n">moduleSetup</span><span class="p">.</span><span class="nf">Get</span><span class="p">()</span> <span class="n">then</span>
            <span class="n">tempApplicationAreaSetup</span><span class="p">.</span><span class="n">Trading</span> <span class="p">:=</span> <span class="n">moduleSetup</span><span class="p">.</span><span class="s">"Application Area Enabled"</span><span class="p">;</span>
    <span class="n">end</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A complete list of the state of all application areas per company would be appreciated. This would give a quick overview of the modules used in each company, and it would even be possible to activate/deactivate them without having to change companies from time to time.</p>

<h1 id="conclusion">Conclusion</h1>

<p>We tend to use <code class="language-plaintext highlighter-rouge">ApplicationArea = All;</code> always and everywhere without thinking about whether the customer will use the module in the company at all. Giving them the ability to determine it themselves makes for a great user experience, which we should enable whenever possible.</p>

<p>This is especially true for ISVs. When developing directly for a customer, it tends to downplay this feature because the new fields should always be displayed until they realize they are not needed in other companies. Then they would hide them if needed through individual personalization.</p>

<p>I would not extend the Experience Tier as I would leave it to the specific licensing scenario <strong>Essential</strong> or <strong>Premium</strong> which should be activated. Remember to enable your custom application area to one or both of these experience tiers and allow the user to toggle the Application Area for the specific companies.</p>

<p>Hope you learned something new.</p>

<p>See you around 🦦</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-extending-application-areas">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-extending-application-areas</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-applicationarea-property">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-applicationarea-property</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlich</name></author><category term="BusinessCentral" /><category term="AL" /><summary type="html"><![CDATA[Create a custom Experience Tier or activate Application Area of a module via a setup table.]]></summary></entry><entry><title type="html">Compile your AL project without docker</title><link href="https://christianbraeunlich.com/businesscentral/2023-05-20/compile-without-docker/" rel="alternate" type="text/html" title="Compile your AL project without docker" /><published>2023-05-20T00:00:00+00:00</published><updated>2023-05-20T00:00:00+00:00</updated><id>https://christianbraeunlich.com/businesscentral/2023-05-20/compile-without-docker</id><content type="html" xml:base="https://christianbraeunlich.com/businesscentral/2023-05-20/compile-without-docker/"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>As I’ve setup a GitHub workflow for e2e testing BC container APIs via newman-cli (postman) last weekend, I used the cmdlet <code class="language-plaintext highlighter-rouge">New-BcContainer</code> from the <strong>BcContainerHelper</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> module to provide a BC container to test against. Nothing too special at this point.</p>

<p>I thought to myself: “Why do we need BC containers to compile our AL project?”. At least this is what I see in many build pipelines: creating a BC container to compile the application.</p>

<p>So let’s just compile without containers.</p>

<h1 id="getting-started">Getting started</h1>

<p>Before we can use the <code class="language-plaintext highlighter-rouge">Compile-AppWithBcCompilerFolder.ps1</code> cmdlet<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> we have to make sure that we can provide it with a <strong>compilerFolder</strong> path.</p>

<h2 id="create-a-compiler-folder">Create a compiler folder</h2>

<p>Create the compiler folder by executing the <code class="language-plaintext highlighter-rouge">New-BcCompilerFolder.ps1</code><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> cmdlet with some additional parameters such as the <strong>artifactUrl</strong>.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">New-BcCompilerFolder</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-artifactUrl</span><span class="w"> </span><span class="p">(</span><span class="n">Get-BCArtifactUrl</span><span class="w"> </span><span class="nt">-type</span><span class="w"> </span><span class="nx">Sandbox</span><span class="w"> </span><span class="nt">-country</span><span class="w"> </span><span class="nx">w1</span><span class="w"> </span><span class="nt">-version</span><span class="w"> </span><span class="nx">20</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>Adjust the <code class="language-plaintext highlighter-rouge">Get-BCArtifactUrl</code> parameters to your project needs.</p>

<p>The <strong>compilerFolder</strong> consists of the following structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>compilerFolder
├── compiler
|   ├── \_rels
|   ├── extension
|   ├── package
|   ├── \[Content\_Types].xml
|   └── extension.vsixmanifest
├── dlls
|   ├── Mock Assemblies    
|   ├── OpenXML    
│   └── Service
└── symbols
    ├── Microsoft_Application_xx.x.xxxxx.xxxx.app
    ├── Microsoft_Application_xx.x.xxxxx.xxxx.json
    ├── Microsoft_Base Application_xx.x.xxxxx.xxxx.app
    ├── Microsoft_Base Application_xx.x.xxxxx.xxxx.app.json
    ├── Microsoft_..._xx.x.xxxxx.xxxx.app
    ├── Microsoft_..._xx.x.xxxxx.xxxx.app.json
    ├── System.app
    └── System.app.json
</code></pre></div></div>

<p>The <strong>compilerFolder</strong> is created in the <em>C:\ProgramData\BcContainerHelper\compiler</em> directory by default. Obviously we don’t have to create the compiler folder every single time. Save the path to the compiler folder or cache the entire folder within your pipeline.</p>

<h2 id="compile-the-app-with-the-compiler-folder">Compile the app with the compiler folder</h2>

<p>We can now use the <code class="language-plaintext highlighter-rouge">Compile-AppWithBcCompilerFolder</code><sup id="fnref:2:1" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> cmdlet to use the compiler folder instead of creating a docker container for app compilation.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Compile-AppWithBcCompilerFolder</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-compilerFolder</span><span class="w"> </span><span class="nv">$compilerFolder</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-appProjectFolder</span><span class="w"> </span><span class="nx">C:\code\AL\JustAnotterApp</span><span class="w">
</span></code></pre></div></div>

<p>The app file is created by default in the <em>output</em> folder in our app project folder after successful compilation.</p>

<p class="notice--info">If you are using <code class="language-plaintext highlighter-rouge">AL-RunPipeline</code><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup> in your pipelines, you might want to have a look at the <code class="language-plaintext highlighter-rouge">useCompilerFolder</code> argument.</p>

<h1 id="breaking-changes">Breaking Changes</h1>

<p>Although this section could be placed in another blog post, I decided it is somewhat related. Some build pipelines would not only compile but also publish and install the app to the container, which again is not primarily the purpose of a build pipeline.</p>

<p>So… AppSourceCop to the rescue!</p>

<p>I’ve prepared a small app which has a dependency to the Base App v20.0. It contains a table object with 2 fields and also a ruleset file to ignore some of the AppSourceCop rules.</p>

<p>Running the following script…</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$compilerFolder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-BcCompilerFolder</span><span class="w"> </span><span class="nt">-artifactUrl</span><span class="w"> </span><span class="p">(</span><span class="n">Get-BCArtifactUrl</span><span class="w"> </span><span class="nt">-type</span><span class="w"> </span><span class="nx">Sandbox</span><span class="w"> </span><span class="nt">-country</span><span class="w"> </span><span class="nx">w1</span><span class="w"> </span><span class="nt">-version</span><span class="w"> </span><span class="nx">20</span><span class="p">)</span><span class="w">
</span><span class="nv">$projectFolder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'C:\code\AL\JustAnotterApp'</span><span class="w">

</span><span class="n">Compile-AppWithBcCompilerFolder</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-compilerFolder</span><span class="w"> </span><span class="nv">$compilerFolder</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-appProjectFolder</span><span class="w"> </span><span class="nv">$projectFolder</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-CopyAppToSymbolsFolder</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-EnableAppSourceCop</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-rulesetFile</span><span class="w"> </span><span class="nv">$projectFolder</span><span class="nx">\al.ruleset.json</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-FailOn</span><span class="w"> </span><span class="nx">error</span><span class="w">
</span></code></pre></div></div>

<p>…leads to this output:</p>

<p align="center">
  <img alt="Search for retention policy via tell me" src="/assets/images/posts/msdyn365bc/2023-08-20-compile-without-docker/output1.png" />
</p>

<p>The app has been created successfully.</p>

<h2 id="set-version-in-appsourcecopjson">Set version in AppSourceCop.json</h2>

<p>We shipped our latest release. Partners and customers would publish and install the app (<em>v0.0.0.0</em>). How does the AppSourceCop know what the previous app is?</p>

<p>We can configure the AppSourceCop with the <strong>AppSourceCop.json</strong><sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup> file. Provide a <strong>version</strong> to check against. This version should be the latest published version of our app.</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.0.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"mandatorySuffix"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CB"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Download the specific app file to the symbols folder. As we set the <strong>CopyAppToSymbolsFolder</strong> in the <em>Compile-AppWithBcCompilerFolder</em> our previous app file (*v0.0.0.0) is already copied to the symbols folder.</p>

<h2 id="make-a-breaking-change">Make a breaking change</h2>

<p>To check if the breaking change is detected, we:</p>
<ul>
  <li>remove the non primary key field from our table object</li>
  <li>increase the version of our app in our <strong>app.json</strong> file.</li>
</ul>

<p>Running the script again…results in the following output:</p>

<p align="center">
  <img alt="Search for retention policy via tell me" src="/assets/images/posts/msdyn365bc/2023-08-20-compile-without-docker/output2.png" />
</p>

<p>Compilation of our app version <em>0.1.0.0</em> is not successful because it detects a breaking change: <em>AS0002: The field with the name ‘Enabled’ and ID ‘2’ was found in the previous version, but is missing in the current extension. This might break the upgrade of existing installations.</em></p>

<h1 id="conclusion">Conclusion</h1>

<p>The scripts shown in this example allow us to compile our app without a docker BC container.</p>

<ul>
  <li>Pros
    <ul>
      <li>faster build speed</li>
      <li>save resources</li>
      <li>runs on Linux</li>
    </ul>
  </li>
  <li>Cons
    <ul>
      <li>not able to run tests this way</li>
      <li>not able to test install/upgrade logic</li>
    </ul>
  </li>
</ul>

<p>If you have any questions or suggestions, feel free to contact me.</p>

<p>Hope you learned something.</p>

<p>See you around 🦦</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://www.powershellgallery.com/packages/BcContainerHelper">https://www.powershellgallery.com/packages/BcContainerHelper</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://github.com/microsoft/navcontainerhelper/blob/master/CompilerFolderHandling/Compile-AppWithBcCompilerFolder.ps1">https://github.com/microsoft/navcontainerhelper/blob/master/CompilerFolderHandling/Compile-AppWithBcCompilerFolder.ps1</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:2:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://github.com/microsoft/navcontainerhelper/blob/master/CompilerFolderHandling/New-BcCompilerFolder.ps1">https://github.com/microsoft/navcontainerhelper/blob/master/CompilerFolderHandling/New-BcCompilerFolder.ps1</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://github.com/microsoft/navcontainerhelper/blob/master/AppHandling/Run-AlPipeline.ps1">https://github.com/microsoft/navcontainerhelper/blob/master/AppHandling/Run-AlPipeline.ps1</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/appsourcecop#configuration">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/appsourcecop#configuration</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlich</name></author><category term="BusinessCentral" /><category term="AL" /><category term="BcContainerHelper" /><summary type="html"><![CDATA[Compile your AL project without having to create a BC container with Docker.]]></summary></entry><entry><title type="html">Setup a BC container and use Traefik to access it</title><link href="https://christianbraeunlich.com/businesscentral/2023-02-02/setup-traefik-for-bc-containers/" rel="alternate" type="text/html" title="Setup a BC container and use Traefik to access it" /><published>2023-02-02T00:00:00+00:00</published><updated>2023-02-02T00:00:00+00:00</updated><id>https://christianbraeunlich.com/businesscentral/2023-02-02/setup-traefik-for-bc-containers</id><content type="html" xml:base="https://christianbraeunlich.com/businesscentral/2023-02-02/setup-traefik-for-bc-containers/"><![CDATA[<p>Hello there 👋</p>

<p>Have you ever wondered how to access your colleague’s local container, so that you can test a new feature right away? Instead of making your <em>local container</em> available we should follow a different approach. Let’s not think of local machines, but rather the cloud or actually temporary resources. Ultimatly, we want to set up a container, deploy our code, provide access to it, test the new feature, then stop and remove the container.</p>

<p>Fair enough… we are going to create a Traefik (https://traefik.io/) container and a <em>Dynamics 365 Business Central</em> container with the help of the <em>BcContainerHelper</em> and some additional docker labels. Our Traefik container than listens on the <em>Docker Deamon</em> to know excactly where to forward the HTTP requests to.</p>

<h1 id="introduction">Introduction</h1>

<p>As a brief introduction, there are multiple ways to make the container accessible. Before we dive into the <em>Getting started</em> section, we want to make sure that we have a rough overview of what the alternatives to Traefik are.</p>

<h2 id="transparent-network">Transparent Network</h2>

<p>Define a transparent network in Docker that you can choose when creating your container. This will handle the container as a physical client in your local network, which comes with some security concerns. As you can already tell, it’s usually not allowed (and shouldn’t be) in your companys network.</p>

<p>The good thing about this is, that each container has its own IP address, making it accessible in your local network without having to spend too much time on managing port mappings.</p>

<p>These are the two steps to create a container in a transparent network.</p>

<ol>
  <li>Create the transparent network</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">docker network create --driver=transparent transparentnetwork</code></p>

<ol>
  <li>Add the network to the additional parameters (<em>New-BcContainer</em>)</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">-additionalParameters  "--network=transparentnetwork"</code></p>

<p class="notice--info">The additional parameters allow us to provide additional docker commands.</p>

<h2 id="port-mapping">Port Mapping</h2>

<p>The Port Mapping from the container to the docker host is a common thing. This might very well fit your needs if you are just working with one docker container.</p>

<p>The basic syntax is as follows: <code class="language-plaintext highlighter-rouge">-p HOST-PORT:CONTAINER-PORT</code></p>

<p>So you might define your additional parameters as follows: <code class="language-plaintext highlighter-rouge">-additionalParameters  "-p 3080:80"</code>.</p>

<p>Now the container port 80 is mapped to the host port 3080, which allows us to access the BC container web client at <code class="language-plaintext highlighter-rouge">http://cbserver:3080</code>.</p>

<p>However, if you were to manage multiple docker containers on one machine, it becomes more and more difficult to keep track of all the used ports. Especially when we are dynamically creating containers, we don’t want to worry about that too much and instead just run the container and access it.</p>

<h2 id="reverse-proxy">Reverse Proxy</h2>

<p>For me, this is the easiest way (once you figured it all out). I recommend this way as it’s easy to setup and forwards all the HTTP requests to the specific container by the rule you defined. You could also provide a signed SSL certificate and provide access to your container from outside your network.</p>

<h1 id="getting-started">Getting started</h1>

<p>For the scope of this blog post, I’d like to keep it simple and just focus on our need to provide access to docker containers within our local network from otter clients. This allows us to develop on one machine and automatically deploy our changes to a docker container on our server.</p>

<h2 id="requirements">Requirements</h2>

<ul>
  <li>Docker</li>
  <li>BcContainerHelper</li>
</ul>

<p class="notice--info">I used <em>Docker Desktop</em> to install Docker on my server. As things turns out, Traefik doesn’t really support it<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>, so you would be better off to just install the <em>Docker (standalone) Engine</em>.</p>

<h2 id="setup">Setup</h2>

<p>We begin with the creation of a Traefik container.</p>

<h3 id="traefik-container">Traefik container</h3>

<p>As <em>Business Central</em> is still running on a windows-based OS, we have to get a Traefik installation based on windows, so that it can than pick-up the correct <em>Docker Deamon</em> that will listen to all the otter windows-based containers, including our <em>Business Central</em> container which we want to expose in our local network.</p>

<p>We actually don’t have to create an image ourselfs, as Tobias Fenster<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> has already done that (see DockerHub: tobiasfenster/traefik-for-windows)<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run -d -p 80:80 -p 443:443 -p 8080:8080 -v C:\traefikforwindows\config:c:\etc\traefik -v \\.\pipe\docker_engine_windows:\\.\pipe\docker_engine_windows tobiasfenster/traefik-for-windows:v2.9.6
</code></pre></div></div>

<p align="center">
  <img alt="Search for retention policy via tell me" src="/assets/images/posts/msdyn365bc/2023-02-02-setup-traefik-for-bc-containers/2023-02-02-setup-traefik-container.png" />
</p>

<p class="notice--info">Depending on your <em>Docker Deamon</em> you might need to change the <em>docker_engine_windows</em> to something else, e.g. <em>docker_engine</em>. You can find the name of your <em>Docker Deamon</em> in the “%USERPROFILE%.docker\daemon.json” file.</p>

<h3 id="bc-container">BC container</h3>

<p>One thing we should keep in mind right now, is the fact that we leverage the Traefik support for listening on <em>Docker Labels</em> (see: https://doc.traefik.io/traefik/providers/docker/). There are also many other so called <em>providers</em> you can use. I definitely recommend you to read the <em>Traefik documentation</em><sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>.</p>

<p>The following script is a minimal version to give you an impression on how it can be done:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$publicDnsName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'cbserver'</span><span class="w">
</span><span class="nv">$containerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'bcserver'</span><span class="w">
</span><span class="nv">$Credential</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">pscredential</span><span class="w"> </span><span class="s1">'admin'</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">ConvertTo-SecureString</span><span class="w"> </span><span class="nt">-String</span><span class="w"> </span><span class="s1">'P@ssword1'</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="p">)</span><span class="w">

</span><span class="nv">$additionalParameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
    </span><span class="s2">"-l </span><span class="se">`"</span><span class="s2">traefik.enable=true</span><span class="se">`"</span><span class="s2">"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"-l </span><span class="se">`"</span><span class="s2">traefik.http.routers.</span><span class="si">$(</span><span class="nv">$containerName</span><span class="si">)</span><span class="s2">-webclient.service=</span><span class="si">$(</span><span class="nv">$containerName</span><span class="si">)</span><span class="s2">-webclient@docker</span><span class="se">`"</span><span class="s2">"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"-l </span><span class="se">`"</span><span class="s2">traefik.http.services.</span><span class="si">$(</span><span class="nv">$containerName</span><span class="si">)</span><span class="s2">-webclient.loadBalancer.server.port=443</span><span class="se">`"</span><span class="s2">"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"-l </span><span class="se">`"</span><span class="s2">traefik.http.services.</span><span class="si">$(</span><span class="nv">$containerName</span><span class="si">)</span><span class="s2">-webclient.loadBalancer.server.scheme=https</span><span class="se">`"</span><span class="s2">"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"-l </span><span class="se">`"</span><span class="s2">traefik.http.routers.</span><span class="si">$(</span><span class="nv">$containerName</span><span class="si">)</span><span class="s2">-webclient.rule=PathPrefix(</span><span class="se">``</span><span class="s2">/</span><span class="si">$(</span><span class="nv">$containerName</span><span class="si">)</span><span class="se">``</span><span class="s2">)</span><span class="se">`"</span><span class="s2">"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"-l </span><span class="se">`"</span><span class="s2">traefik.http.routers.</span><span class="si">$(</span><span class="nv">$containerName</span><span class="si">)</span><span class="s2">-webclient.entrypoints=websecure</span><span class="se">`"</span><span class="s2">"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"-l </span><span class="se">`"</span><span class="s2">traefik.http.routers.</span><span class="si">$(</span><span class="nv">$containerName</span><span class="si">)</span><span class="s2">-webclient.tls=true</span><span class="se">`"</span><span class="s2">"</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="n">New-BcContainer</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-accept_eula</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-auth</span><span class="w"> </span><span class="nx">NavUserPassword</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-artifactUrl</span><span class="w"> </span><span class="p">(</span><span class="n">Get-BCArtifactUrl</span><span class="w"> </span><span class="nt">-type</span><span class="w"> </span><span class="nx">OnPrem</span><span class="w"> </span><span class="nt">-country</span><span class="w"> </span><span class="s1">'us'</span><span class="w"> </span><span class="nt">-version</span><span class="w"> </span><span class="s1">'21'</span><span class="p">)</span><span class="w"> </span><span class="err">`</span><span class="w">
    </span><span class="nt">-containerName</span><span class="w"> </span><span class="nv">$containerName</span><span class="w"> </span><span class="err">`</span><span class="w">
    </span><span class="nt">-Credential</span><span class="w"> </span><span class="nv">$Credential</span><span class="w"> </span><span class="err">`</span><span class="w">
    </span><span class="nt">-isolation</span><span class="w"> </span><span class="n">hyperv</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-shortcuts</span><span class="w"> </span><span class="nx">None</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-updateHosts</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-additionalParameters</span><span class="w"> </span><span class="nv">$additionalParameters</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-multitenant</span><span class="p">:</span><span class="bp">$false</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-useSSL</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-PublicDnsName</span><span class="w"> </span><span class="nv">$publicDnsName</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-installCertificateOnHost</span><span class="w">
</span></code></pre></div></div>

<p>Remember that we ultimately want to define rules for all the other ports as well, such as OData, SOAP, devEndpoint, etc. In order for you to access all the other <em>Business Central</em> components<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup> you need to add additional docker labels to that container. This is neccessary to connect to the different ports. Maybe I’ll provide a full script for that later…</p>

<h2 id="access-your-bc-container-from-another-client">Access your BC container from another client</h2>

<p>At this point we can access the container from a different host machine by sending the HTTP request to the previously defined path prefix rule <strong>/bcserver</strong>. As my server is named <strong>cbserver</strong> (..apologies for the confusing names at this point..) the full path I’d send the request to is: <code class="language-plaintext highlighter-rouge">https://cbserver/bcserver</code></p>

<p align="center">
  <img alt="Search for retention policy via tell me" src="/assets/images/posts/msdyn365bc/2023-02-02-setup-traefik-for-bc-containers/2023-02-02-bc-container-remote-webclient.png" />
</p>

<p>You can already see that Traefik, in combination with a CI/CD pipeline that deploys your code at a certain stage, can be a powerful ally that allows you to provide access to these newly created containers. These can be named after the ID of the Pull Request in combination with the shortcut name of your extension app for example.</p>

<p>You can think of it like GitHub Codespaces, which basically allows you to check out a branch into a newly created container and continue developing based on that container and also access the web server. I’ll follow up on that with a blog post soon.</p>

<h1 id="conclusion">Conclusion</h1>

<p>It’s very easy to make your containers accessible in your internal network so that otter clients can access the web-client and start testing the features. With the reverse proxy its also possible to make your containers available outside of your own network for example to demo it to your customers.</p>

<p>Make sure to watch the following presentation of Tobias Fenster<sup id="fnref:2:1" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> on NAV TechDays 2019. He also explains the usage of Container Orchestration in his scenario, which is taking it one step further.</p>

<!-- Courtesy of embedresponsively.com //-->

<div class="responsive-video-container">
    <iframe src="https://www.youtube-nocookie.com/embed/Dr6bFoRELnY" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
  </div>

<p>Hope you learned something.</p>

<p>See you around 🦦</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://github.com/microsoft/navcontainerhelper/issues/2839">https://github.com/microsoft/navcontainerhelper/issues/2839</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://tobiasfenster.io/">https://tobiasfenster.io/</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:2:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://hub.docker.com/r/tobiasfenster/traefik-for-windows">https://hub.docker.com/r/tobiasfenster/traefik-for-windows</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://tobiasfenster.io/running-traefik-in-a-container-on-a-modern-windows-server-version">https://tobiasfenster.io/running-traefik-in-a-container-on-a-modern-windows-server-version</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p><a href="https://doc.traefik.io/traefik/">https://doc.traefik.io/traefik/</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/deployment/product-and-architecture-overview">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/deployment/product-and-architecture-overview</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlich</name></author><category term="BusinessCentral" /><category term="Docker" /><category term="Traefik" /><category term="BcContainerHelper" /><summary type="html"><![CDATA[Create BC Containers and use Traefik to route your HTTP requests to the respective container.]]></summary></entry><entry><title type="html">Setup local Business Central - Contribution Pilot</title><link href="https://christianbraeunlich.com/businesscentral/2022-12-29/setup-contribution-pilot/" rel="alternate" type="text/html" title="Setup local Business Central - Contribution Pilot" /><published>2022-12-29T00:00:00+00:00</published><updated>2022-12-29T00:00:00+00:00</updated><id>https://christianbraeunlich.com/businesscentral/2022-12-29/setup-contribution-pilot</id><content type="html" xml:base="https://christianbraeunlich.com/businesscentral/2022-12-29/setup-contribution-pilot/"><![CDATA[<p>Hello there 👋</p>

<p>as you may already know, Microsoft announced in September ‘22 at <em>BC TechDays</em><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> in the <strong>Collaborating on Business Central’s open source platform</strong><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> session they are adding community members to their contribution pilot. If you didn’t make it to the eventor session, don’t worry. You can watch the sessions on the <em>@mibuso.com</em> YouTube-Channel<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>.</p>

<!-- Courtesy of embedresponsively.com //-->

<div class="responsive-video-container">
    <iframe src="https://www.youtube-nocookie.com/embed/X1OyTEpTmfw" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
  </div>

<p>They also provide all previous <em>NAV TechDays</em> sessions in their YouTube-Playlist<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>. I definetly recommend you to visit their channel<sup id="fnref:3:1" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>. Their is so much valuable knowledge. A huge thanks to the organizers, partners and speakers making this event possible!</p>

<p>Before we start, you may have asked yourself: <em>“Why would you add it yourself? Isn’t this Microsoft’s job?”</em> Good question. Personally, I think there are so many possible answers to this, that I’ll leave the topic open for another blog post. For now, I would just leave this tweet by <em>Natalie Karolak</em><sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>, which more or less covers the topic imho:</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-11-09-tweet-natalie-karolak.png" alt="Tweet by Natalie Karolak" /></p>

<p><em>Source: https://twitter.com/KarolakNatalie/status/1590375302227972099</em></p>

<p>Well… this blog post is not about the whole contribution pilot project itself. I think that <strong>Open Source</strong> is a huge step and a big opportunity for the BC Community to make the product even better than it already is today. But that’s another story.</p>

<hr />

<p>Let’s focus on the technial part itself. As with any new project, chances are high that you’ll need to setup some requirements before you can develop <em>locally</em>. Especially in this different ecosystem it might not be as intuitive if you’ve never done it before. It’s not our usual environment, as we would normally load the dependenies by setting the <em>application</em> and <em>plattform</em> property in our <em>app.json</em> manifest file.</p>

<p>In the following steps, I describe how to setup a local development environment, so that you would be able to make changes to the <em>Base Application</em> and publish these changes.</p>

<h3 id="setup-local-development-environment">Setup local development environment</h3>

<p>This whole step is rather optional, since you most likely already have one or multiple containers up-and-running.</p>

<p>In the <em>BC TechDays sesion</em><sup id="fnref:2:1" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> they run the <code class="language-plaintext highlighter-rouge">./Modules/.AL-Go/localDevEnv.ps1</code><sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup> PowerShell script. However, I use my own PowerShell Script <em>New-BcContainer.ps1</em> which you can find here: <a href="https://github.com/christianbraeunlich/bccontainerhelper-scripts">https://github.com/christianbraeunlich/bccontainerhelper-scripts</a>. The repository itself, follows the wise words of <em>Freddy Kristiansen</em>: “Keep the script, not the Container”<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">7</a></sup>.</p>

<p class="notice--info">You need the PowerShell module <strong>BcContainerHelper</strong><sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">8</a></sup> installed and imported.</p>

<p>Ofcourse you can use your own script or simply use the provided script by Microsoft:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$containerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'mydemo'</span><span class="w">
</span><span class="nv">$password</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'P@ssw0rd'</span><span class="w">
</span><span class="nv">$securePassword</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ConvertTo-SecureString</span><span class="w"> </span><span class="nt">-String</span><span class="w"> </span><span class="nv">$password</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="nv">$credential</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">pscredential</span><span class="w"> </span><span class="s1">'admin'</span><span class="p">,</span><span class="w"> </span><span class="nv">$securePassword</span><span class="w">
</span><span class="nv">$auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'UserPassword'</span><span class="w">
</span><span class="nv">$artifactUrl</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-BcArtifactUrl</span><span class="w"> </span><span class="nt">-type</span><span class="w"> </span><span class="s1">'OnPrem'</span><span class="w"> </span><span class="nt">-country</span><span class="w"> </span><span class="s1">'w1'</span><span class="w"> </span><span class="nt">-select</span><span class="w"> </span><span class="s1">'Latest'</span><span class="w">
</span><span class="nv">$licenseFile</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;Path to license file&gt;'</span><span class="w">
</span><span class="n">New-BcContainer</span><span class="w"> </span><span class="nt">-accept_eula</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-containerName</span><span class="w"> </span><span class="nv">$containerName</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-credential</span><span class="w"> </span><span class="nv">$credential</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-auth</span><span class="w"> </span><span class="nv">$auth</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-artifactUrl</span><span class="w"> </span><span class="nv">$artifactUrl</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-includeTestToolkit</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-includeTestLibrariesOnly</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-licenseFile</span><span class="w"> </span><span class="nv">$licenseFile</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-includeAL</span><span class="w"> </span><span class="nt">-doNotExportObjectsToText</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-updateHosts</span><span class="w">
</span></code></pre></div></div>

<p><em>Source: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-set-up-an-environment#set-up-a-business-central-docker-container</em></p>

<p>Just make sure to select <strong>w1</strong> as <em>country</em>.</p>

<p class="notice--info">For the provided code example to work you need to provide a valid developer license.</p>

<p>Now let’s move on to the actual steps.</p>

<h3 id="clone-the-git-repository">Clone the git repository</h3>

<p>Before you can clone the GitHub repository<sup id="fnref:9" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">9</a></sup> you need access to it. To get invited you need to contact for example <em>Jesper Schulz-Wedder</em><sup id="fnref:10" role="doc-noteref"><a href="#fn:10" class="footnote" rel="footnote">10</a></sup> as he mentioned in one of his tweets:</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-09-16-tweet-jesper.png" alt="Tweet by Jesper" /></p>

<p><em>Source: https://twitter.com/JesperSchulz/status/1570770745491587073</em></p>

<p class="notice--info">By the time of writing it is a private repository. It may be that the repository will be made public at some point in the future.</p>

<p>The following code snippet can be used in your terminal. Just change the directory and enter the following commands:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/microsoft/BusinessCentralApps
<span class="nb">cd </span>BusinessCentralApps
code <span class="nb">.</span>
</code></pre></div></div>

<p>A successful cloning step looks similar to this terminal output:</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-git-clone.png" alt="Publishing Success" /></p>

<p>We change our directory to <em>BusinessCentralApps</em>. The command <code class="language-plaintext highlighter-rouge">code .</code> will open the current directory in <strong>VS Code</strong> and we will see the following structure in the <em>VS Code Explorer</em>:</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-vscode-businesscentralapps.png" alt="Publishing Success" /></p>

<p>At this point, the opened folder contains multiple projects, so we need to open the workspace.</p>

<h3 id="open-the-workspace">Open the Workspace</h3>

<p>As we like to make contributions to the <em>Base Application</em>, we can simply open the predefined workspace by opening the <strong>BaseAndTests.code-workspace</strong> file. Enter <code class="language-plaintext highlighter-rouge">CTRL + P</code> and search for <em>code-workspace</em>. Click on <em>BaseAndTests.code-workspace</em>.</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-code-workspace.png" alt="Navigate to the BaseAndTests.code-workspace file" /></p>

<p>After opening the <em>BaseAndTests.code-workspace</em> file the content looks similar to this:</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-baseandtests-code-workspace.png" alt="Navigate to the BaseAndTests.code-workspace file" /></p>

<p>Now click on the button in the bottom right corner that says <strong>Open Workspace</strong>.</p>

<h3 id="update-settings">Update settings</h3>

<p>In order for the <em>AL Language</em> extension to find all the necessary <strong>DotNet</strong> libraries and to be able to compile the app, we need to add/update the following settings: <a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-set-up-an-environment">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-set-up-an-environment</a>.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"al.assemblyProbingPaths"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
  </span><span class="s2">"C:/Windows/Microsoft.NET/Framework64/v4.0.30319"</span><span class="p">,</span><span class="w">
  </span><span class="s2">"C:/bcartifacts.cache/sandbox/21.2.49946.51329"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">type</span><span class="w"> </span><span class="err">and</span><span class="w"> </span><span class="err">version</span><span class="w"> </span><span class="err">number</span><span class="w"> </span><span class="err">depends</span><span class="w"> </span><span class="err">on</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">downloaded</span><span class="w"> </span><span class="err">artifact</span><span class="w">
  </span><span class="s2">"C:/WINDOWS/assembly"</span><span class="w">
</span><span class="p">]</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<p>With a project that big we’d like to further optimize the performance of our <strong>VS Code</strong> editor. You can follow the instructions provided by the documentation: <a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-optimize-visual-studio-code">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-optimize-visual-studio-code</a>.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"al.backgroundCodeAnalysis"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="err">,</span><span class="w">
</span><span class="nl">"al.enableCodeActions"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="err">,</span><span class="w">
</span><span class="nl">"al.enableCodeAnalysis"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="err">,</span><span class="w">
</span><span class="nl">"al.incrementalBuild"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="err">,</span><span class="w">
</span><span class="nl">"editor.codeLens"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="err">,</span><span class="w">
</span><span class="nl">"[al]"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nl">"editor.formatOnSave"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>After applying all the changes, your <code class="language-plaintext highlighter-rouge">.vscode/settings.json</code> file might look similar to this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="err">...</span><span class="w">
  </span><span class="nl">"al.assemblyProbingPaths"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"C:/Windows/Microsoft.NET/Framework64/v4.0.30319"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"C:/bcartifacts.cache/sandbox/21.2.49946.51329"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">type</span><span class="w"> </span><span class="err">and</span><span class="w"> </span><span class="err">version</span><span class="w"> </span><span class="err">number</span><span class="w"> </span><span class="err">depends</span><span class="w"> </span><span class="err">on</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">downloaded</span><span class="w"> </span><span class="err">artifact</span><span class="w">
    </span><span class="s2">"C:/WINDOWS/assembly"</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"al.backgroundCodeAnalysis"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"al.enableCodeActions"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"al.enableCodeAnalysis"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"al.incrementalBuild"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"editor.codeLens"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"[al]"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"editor.formatOnSave"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="err">...</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>But… nowadays, especially because there are +20 paths in the <code class="language-plaintext highlighter-rouge">.code-workspace</code> file, I recommend to add the <em>launch</em> and the <em>settings</em> keys to the <strong>.code-workspace</strong> file. Defining the <em>launch</em> property in here is possible since <em>AL Language</em> version <em>10.1</em> (see the changelog<sup id="fnref:11" role="doc-noteref"><a href="#fn:11" class="footnote" rel="footnote">11</a></sup>). If you need a more in-depth description, ZHU<sup id="fnref:12" role="doc-noteref"><a href="#fn:12" class="footnote" rel="footnote">12</a></sup> got you covered in one of his amazing blog post: <a href="https://yzhums.com/32067/">https://yzhums.com/32067/</a>. In short: the information is inherited by all projects, so we end up with only one configuration file that works for all paths.</p>

<p>To add the previous configuration to the workspace settings, open the <em>command pallet</em> (<strong>F1</strong> or <strong>CTRL + SHIFT + P</strong>) in an opened workspace and search for <em>workspace settings</em>. Click on the be prompted option <strong>Preferences: Open Workspace Settings (JSON)</strong>. This will open the <strong>BaseAppTests.code-workspace</strong> file to which we can now add all the informations needed for us to compile and publish the app in the next steps. This is how the file could look like after applying the changes:</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-vscode-update-baseandtests-code-workspace.png" alt="Updated BaseAndTests.code-workspace" /></p>

<p>Which way you choose depends on your preferences.</p>

<p>At this point you can try and publish the <em>Base Application</em>, which most likely will end up with errors that we will <em>fix</em> in the next step.</p>

<h3 id="customize-the-appjson">Customize the app.json</h3>

<p>We need to edit the <code class="language-plaintext highlighter-rouge">app.json</code> before we can publish the app to our local container. Enter <code class="language-plaintext highlighter-rouge">CTRL + P</code> on your keyboard and search for <code class="language-plaintext highlighter-rouge">app.json base</code>. The additional <em>base</em> keyword makes sure to filter the search results. Otherwise we would get too many files to choose from … <em>efficiency is key</em>.</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-appjson-baseapp.png" alt="Updated app.json" /></p>

<h4 id="change-version">change <code class="language-plaintext highlighter-rouge">version</code></h4>

<p>As the version numbers only change in their major and minor parts we can already assume that the installed version is higher due to the build and revision parts. All about the version numbers in Business Central you can read here: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/version-numbers</p>

<p>To make sure the version we publish is higher we can increase the major or minor part. I choose to increase the major part so that I don’t have to worry about it any time soon.</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-appjson-version.png" alt="Edit version in app.json" /></p>

<p>Fun fact: with this version number in place I might get some trouble with it by the year ~2062 when this version number becomes lower than the installed version.</p>

<h4 id="add-runtime">add <code class="language-plaintext highlighter-rouge">runtime</code></h4>

<p>I’ve had to lower the runtime version to <code class="language-plaintext highlighter-rouge">10.1</code>, as the default runtime (11.0 in this case) is not supported by the previously setup local container. You can try publishing it without adding this property.</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-appjson-runtime.png" alt="Edit version in app.json" /></p>

<p>Additional information about choosing a runtime version can be found here: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-choosing-runtime</p>

<h3 id="correct-al-language-version">Correct <em>AL Language</em> version</h3>

<p>One step that is often completely ignored is that we should develop with the correct <code class="language-plaintext highlighter-rouge">AL Language</code> version. We can do this by downloading the <code class="language-plaintext highlighter-rouge">ALLanguage.vsix</code> file from our previously created container by navigating to <code class="language-plaintext highlighter-rouge">http://containername/ALLanguage.vsix</code> via the browser of choice.</p>

<p>After we successfully downloaded the file, open <em>VS Code</em>. Navigate to <em>Extensions</em> and select <strong>Install from VSIX…</strong>. Select the downloaded file <em>ALLanguage.vsix</em>. Now reload <em>VS Code</em>.</p>

<h3 id="publish">Publish</h3>

<p>Select a file that is included in the <em>BaseApp</em> path of the opened Workspace. This way we make sure that we will publish the correct extension.</p>

<p>Now publish (without debugging) (<strong>CTRL + F5</strong>).</p>

<p class="notice--info">This might take a moment, depending on your hardware specs and current usage of your host machine. Would highly recommend you to have more than 16GB of memory.</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-success.png" alt="Publishing Success" /></p>

<p>Publishing took about +8 minutes for me (i5-6300U &amp; 16GB RAM).</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-29-setup-contribution-pilot/2022-12-29-extension-management.png" alt="Publishing Success" /></p>

<p>At this point you are ready to develop your first contribution to the contribution pilot. Please refer to the <strong>CONTRIBUTING.md</strong><sup id="fnref:13" role="doc-noteref"><a href="#fn:13" class="footnote" rel="footnote">13</a></sup> in the <em>microsoft/BusinessCentralApps</em> GitHub repository<sup id="fnref:9:1" role="doc-noteref"><a href="#fn:9" class="footnote" rel="footnote">9</a></sup> on how to contribute.</p>

<p>Hope you learned something.</p>

<p>See you around 🦦</p>

<hr />

<p>Credits to <em>Frédéric Vercaemst</em><sup id="fnref:14" role="doc-noteref"><a href="#fn:14" class="footnote" rel="footnote">14</a></sup> who provided useful tips in the <em>BusinessCentralApps</em> discussion<sup id="fnref:15" role="doc-noteref"><a href="#fn:15" class="footnote" rel="footnote">15</a></sup>.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://www.bctechdays.com/archive/bc-techdays-2022">https://www.bctechdays.com/archive/bc-techdays-2022</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://www.youtube.com/watch?v=X1OyTEpTmfw">https://www.youtube.com/watch?v=X1OyTEpTmfw</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:2:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://www.youtube.com/@mibusocom">https://www.youtube.com/@mibusocom</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:3:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://www.youtube.com/@mibusocom/playlists">https://www.youtube.com/@mibusocom/playlists</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p><a href="https://twitter.com/KarolakNatalie">https://twitter.com/KarolakNatalie</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:6" role="doc-endnote">
      <p><a href="https://github.com/microsoft/AL-Go-PTE/tree/main/.AL-Go">https://github.com/microsoft/AL-Go-PTE/tree/main/.AL-Go</a> <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:7" role="doc-endnote">
      <p><a href="https://freddysblog.com/2020/10/12/troubleshooting-business-central-on-docker/">https://freddysblog.com/2020/10/12/troubleshooting-business-central-on-docker/</a> <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:8" role="doc-endnote">
      <p><a href="https://www.powershellgallery.com/packages/BcContainerHelper">https://www.powershellgallery.com/packages/BcContainerHelper</a> <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:9" role="doc-endnote">
      <p><a href="https://github.com/microsoft/BusinessCentralApps">https://github.com/microsoft/BusinessCentralApps</a> <a href="#fnref:9" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:9:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:10" role="doc-endnote">
      <p><a href="https://twitter.com/JesperSchulz">https://twitter.com/JesperSchulz</a> <a href="#fnref:10" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:11" role="doc-endnote">
      <p><a href="https://marketplace.visualstudio.com/items/ms-dynamics-smb.al/changelog">https://marketplace.visualstudio.com/items/ms-dynamics-smb.al/changelog</a> <a href="#fnref:11" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:12" role="doc-endnote">
      <p><a href="https://twitter.com/yzhums">https://twitter.com/yzhums</a> <a href="#fnref:12" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:13" role="doc-endnote">
      <p><a href="https://github.com/microsoft/BusinessCentralApps/blob/main/CONTRIBUTING.md">https://github.com/microsoft/BusinessCentralApps/blob/main/CONTRIBUTING.md</a> <a href="#fnref:13" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:14" role="doc-endnote">
      <p><a href="https://twitter.com/fredericvercae1">https://twitter.com/fredericvercae1</a> <a href="#fnref:14" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:15" role="doc-endnote">
      <p><a href="https://github.com/microsoft/BusinessCentralApps/discussions/36#discussioncomment-4017093">https://github.com/microsoft/BusinessCentralApps/discussions/36#discussioncomment-4017093</a> <a href="#fnref:15" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlichf</name></author><category term="BusinessCentral" /><category term="AL" /><category term="Open Source" /><summary type="html"><![CDATA[How to contribute to Microsoft Dynamics 365 Business Central - Application Platform Contribution Pilot?]]></summary></entry><entry><title type="html">Retention Policy: The clean up job queue</title><link href="https://christianbraeunlich.com/businesscentral/2022-12-17/retention-policy/" rel="alternate" type="text/html" title="Retention Policy: The clean up job queue" /><published>2022-12-17T00:00:00+00:00</published><updated>2022-12-17T00:00:00+00:00</updated><id>https://christianbraeunlich.com/businesscentral/2022-12-17/retention-policy</id><content type="html" xml:base="https://christianbraeunlich.com/businesscentral/2022-12-17/retention-policy/"><![CDATA[<p>Hello there 👋</p>

<p>Data is essential to any ERP system, unless the data is irrelevant or so old that it’s no longer needed (depending on your local policies). This is true whether you archive some records or create log entries for a feature of your extension. If these records would also store files in a blob field aswell, chances are that sooner or later you will run out of storage space.</p>

<p>As of Version 17.x (that’s already two years ago) Microsoft introduced the possibility to <strong>Define retention policies</strong><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. Pretty big deal as this is a big productivity boost for developers and a good way for customers to keep control of the data (e.g. how long the data would be allowed to stay in the system, by defining a <strong>Retention Period</strong>).</p>

<p class="notice--info">If there is a risk of data loss, it is advisable to contact the extension publisher. Conclusion: only allow tables where the data is not important at all.</p>

<p>You can find the <strong>Retention Policies</strong> page by searching for <em>retention</em> via <strong>Tell Me</strong>:</p>

<p align="center">
  <img alt="Search for retention policy via tell me" src="/assets/images/posts/msdyn365bc/2022-12-17-retention-policy/2022-12-17-retention-policy-tell-me.png" />
</p>

<p>After opening the <strong>Retention Policies</strong> page you will be displayed with the already created records. The list also gives you a quick overview of which records are enabled and which <strong>Retention Period</strong> is set for the individual entry:</p>

<p><img src="/assets/images/posts/msdyn365bc/2022-12-17-retention-policy/2022-12-17-retention-policies.png" alt="Retention policies list page" /></p>

<p>As this list is company specific, it’s important to make use of an event that is triggered either after login or after initialization of a new company. This way we ensure that the tables are allowed for newly created companies. Check out <a href="https://blog.christianbraeunlich.com/business%20central/2022-09-27/system-initialization/">my last blog post</a>) for examples on <em>Company-Initialize</em>.</p>

<p>Without further ado, let’s take a look at an example:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">codeunit</span> <span class="m">50100</span> <span class="s">"Module Installer"</span>
<span class="p">{</span>
    <span class="n">Subtype</span> <span class="p">=</span> <span class="n">Install</span><span class="p">;</span>
    <span class="n">Access</span> <span class="p">=</span> <span class="n">Internal</span><span class="p">;</span>
    <span class="n">Permissions</span> <span class="p">=</span> <span class="n">tabledata</span> <span class="n">Field</span> <span class="p">=</span> <span class="n">r</span><span class="p">;</span>

    <span class="n">trigger</span> <span class="nf">OnInstallAppPerCompany</span><span class="p">()</span>
    <span class="n">begin</span>
        <span class="nf">AddRetentionPolicyAllowedTables</span><span class="p">();</span>
    <span class="n">end</span><span class="p">;</span>

    <span class="n">procedure</span> <span class="nf">AddRetentionPolicyAllowedTables</span><span class="p">()</span>
    <span class="n">begin</span>
        <span class="nf">AddRetentionPolicyAllowedTables</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
    <span class="n">end</span><span class="p">;</span>

    <span class="n">procedure</span> <span class="nf">AddRetentionPolicyAllowedTables</span><span class="p">(</span><span class="n">ForceUpdate</span><span class="p">:</span> <span class="n">Boolean</span><span class="p">)</span>
    <span class="kt">var</span>
        <span class="n">Field</span><span class="p">:</span> <span class="n">Record</span> <span class="n">Field</span><span class="p">;</span>
        <span class="n">UpgradeTag</span><span class="p">:</span> <span class="n">Codeunit</span> <span class="s">"Upgrade Tag"</span><span class="p">;</span>
        <span class="n">RetenPolAllowedTables</span><span class="p">:</span> <span class="n">Codeunit</span> <span class="s">"Reten. Pol. Allowed Tables"</span><span class="p">;</span>
        <span class="n">IsInitialSetup</span><span class="p">:</span> <span class="n">Boolean</span><span class="p">;</span>
    <span class="n">begin</span>
        <span class="n">IsInitialSetup</span> <span class="p">:=</span> <span class="n">not</span> <span class="n">UpgradeTag</span><span class="p">.</span><span class="nf">HasUpgradeTag</span><span class="p">(</span><span class="nf">GetEmailTablesAddedToAllowedListUpgradeTag</span><span class="p">());</span>
        <span class="k">if</span> <span class="nf">not</span> <span class="p">(</span><span class="n">IsInitialSetup</span> <span class="n">or</span> <span class="n">ForceUpdate</span><span class="p">)</span> <span class="n">then</span>
            <span class="n">exit</span><span class="p">;</span>

        <span class="n">RetenPolAllowedTables</span><span class="p">.</span><span class="nf">AddAllowedTable</span><span class="p">(</span><span class="n">Database</span><span class="p">::</span><span class="s">"Demo Log"</span><span class="p">);</span>

        <span class="k">if</span> <span class="n">IsInitialSetup</span> <span class="n">then</span>
            <span class="n">UpgradeTag</span><span class="p">.</span><span class="nf">SetUpgradeTag</span><span class="p">(</span><span class="nf">GetEmailTablesAddedToAllowedListUpgradeTag</span><span class="p">());</span>
    <span class="n">end</span><span class="p">;</span>

    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">GetEmailTablesAddedToAllowedListUpgradeTag</span><span class="p">():</span> <span class="n">Code</span><span class="p">[</span><span class="m">250</span><span class="p">]</span>
    <span class="n">begin</span>
        <span class="nf">exit</span><span class="p">(</span><span class="err">'</span><span class="n">CB</span><span class="p">-</span><span class="m">1234</span><span class="p">-</span><span class="n">DemoLogEntryAdded</span><span class="p">-</span><span class="m">20221217</span><span class="err">'</span><span class="p">);</span>
    <span class="n">end</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="n">Codeunit</span><span class="p">::</span><span class="s">"Reten. Pol. Allowed Tables"</span><span class="p">,</span> <span class="err">'</span><span class="n">OnRefreshAllowedTables</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">false</span><span class="p">)]</span>
    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">AddAllowedTablesOnRefreshAllowedTables</span><span class="p">()</span>
    <span class="n">begin</span>
        <span class="nf">AddRetentionPolicyAllowedTables</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
    <span class="n">end</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="n">Codeunit</span><span class="p">::</span><span class="s">"System Initialization"</span><span class="p">,</span> <span class="err">'</span><span class="n">OnAfterLogin</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">false</span><span class="p">)]</span>
    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">AddAllowedTablesOnAfterSystemInitialization</span><span class="p">()</span>
    <span class="n">begin</span>
        <span class="nf">AddRetentionPolicyAllowedTables</span><span class="p">();</span>
    <span class="n">end</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The code snippet above is a slightly modified copy of the <a href="https://github.com/microsoft/ALAppExtensions/blob/main/Modules/System/Email/src/EmailInstaller.Codeunit.al">1596 “Email Installer”</a> codeunit. There is a lot of overhead in this codeunit, I agree. The only relevant line for now is <code class="language-plaintext highlighter-rouge">RetenPolAllowedTables.AddAllowedTable(Database::"Demo Log");</code>. This will add our custom <strong>Demo Log</strong> table to the allowed tables that can be picked up by the <strong>Retention Policy</strong>. Only now we are able to add a new <strong>Retention Policy</strong> based on that table.</p>

<p>This simplified example is probably not sufficient because you only want to refer to specific records in this table. That’s where the procedure <code class="language-plaintext highlighter-rouge">AddTableFilterToJsonArray</code> comes in. But first take a look at the <code class="language-plaintext highlighter-rouge">AddAllowedTable</code> procedures from the <a href="https://github.com/microsoft/ALAppExtensions/blob/main/Modules/System/Retention%20Policy/src/Retention%20Policy%20Allowed%20Tables/RetenPolAllowedTables.Codeunit.al">3905 “Reten. Pol. Allowed Tables”</a> codeunit. Thanks to overloading we are looking for the procedure <code class="language-plaintext highlighter-rouge">AddAllowedTable(TableId: Integer; TableFilters: JsonArray): Boolean</code>. If you want to set the <em>mandatory minimum retention days</em> and still be able to filter you have to provide a few more parameters to the procedure <code class="language-plaintext highlighter-rouge">AddAllowedTable(TableId: Integer; DefaultDateFieldNo: Integer; MandatoryMinRetenDays: Integer; RetenPolFiltering: Enum "Reten. Pol. Filtering"; RetenPolDeleting: Enum "Reten. Pol. Deleting"; TableFilters: JsonArray): Boolean</code>.</p>

<p>The official documentation of the <a href="https://learn.microsoft.com/en-us/dynamics365/business-central/admin-data-retention-policies">Define Data Retention Policies</a><sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> provides the following example which includes a mandatory filter:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">trigger</span> <span class="nf">OnInstallAppPerCompany</span><span class="p">()</span>
<span class="kt">var</span>
    <span class="n">ChangeLogEntry</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Change Log Entry"</span><span class="p">;</span>
    <span class="n">RetenPolAllowedTables</span><span class="p">:</span> <span class="n">Codeunit</span> <span class="s">"Reten. Pol. Allowed Tables"</span><span class="p">;</span>
    <span class="n">RetentionPeriod</span><span class="p">:</span> <span class="n">Enum</span> <span class="s">"Retention Period Enum"</span><span class="p">;</span>
    <span class="n">RecRef</span><span class="p">:</span> <span class="n">RecordRef</span><span class="p">;</span>
    <span class="n">TableFilters</span><span class="p">:</span> <span class="n">JsonArray</span><span class="p">;</span>
    <span class="n">Enabled</span><span class="p">:</span> <span class="n">Boolean</span><span class="p">;</span>
    <span class="n">Mandatory</span><span class="p">:</span> <span class="n">Boolean</span><span class="p">;</span>
<span class="n">begin</span>
    <span class="n">ChangeLogEntry</span><span class="p">.</span><span class="nf">Reset</span><span class="p">();</span>
    <span class="n">ChangeLogEntry</span><span class="p">.</span><span class="nf">SetFilter</span><span class="p">(</span><span class="s">"Field Log Entry Feature"</span><span class="p">,</span> <span class="err">'</span><span class="p">%</span><span class="m">1</span><span class="p">|%</span><span class="m">2</span><span class="err">'</span><span class="p">,</span> <span class="n">ChangeLogEntry</span><span class="p">.</span><span class="s">"Field Log Entry Feature"</span><span class="p">::</span><span class="s">"Monitor Sensitive Fields"</span><span class="p">,</span> <span class="n">ChangeLogEntry</span><span class="p">.</span><span class="s">"Field Log Entry Feature"</span><span class="p">::</span><span class="n">All</span><span class="p">);</span>
    <span class="n">RecRef</span><span class="p">.</span><span class="nf">GetTable</span><span class="p">(</span><span class="n">ChangeLogEntry</span><span class="p">);</span>
    <span class="n">Enabled</span> <span class="p">:=</span> <span class="k">true</span><span class="p">;</span>
    <span class="n">Mandatory</span> <span class="p">:=</span> <span class="k">true</span><span class="p">;</span>
    <span class="n">RetenPolAllowedTables</span><span class="p">.</span><span class="nf">AddTableFilterToJsonArray</span><span class="p">(</span><span class="n">TableFilters</span><span class="p">,</span> <span class="n">RetentionPeriod</span><span class="p">::</span><span class="s">"28 Days"</span><span class="p">,</span> <span class="n">ChangeLogEntry</span><span class="p">.</span><span class="nf">FieldNo</span><span class="p">(</span><span class="n">SystemCreatedAt</span><span class="p">),</span> <span class="n">Enabled</span><span class="p">,</span> <span class="n">Mandatory</span><span class="p">,</span> <span class="n">RecRef</span><span class="p">);</span>
    <span class="n">RetenPolAllowedTables</span><span class="p">.</span><span class="nf">AddAllowedTable</span><span class="p">(</span><span class="n">Database</span><span class="p">::</span><span class="s">"Change Log Entry"</span><span class="p">,</span> <span class="n">ChangeLogEntry</span><span class="p">.</span><span class="nf">FieldNo</span><span class="p">(</span><span class="n">SystemCreatedAt</span><span class="p">),</span> <span class="n">TableFilters</span><span class="p">);</span>
<span class="n">end</span><span class="p">;</span>
</code></pre></div></div>

<p>At this point I would like to recommend to you to check out the blog post of Stefano Demiliani - <a href="https://demiliani.com/2020/11/18/dynamics-365-business-central-creating-retention-policies-from-al/">Dynamics 365 Business Central: creating retention policies from AL</a><sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> and the blog post of Krzysztof Bialowas - <a href="https://www.mynavblog.com/2020/12/21/clean-after-yourself-a-few-words-about-retention-policies/">Clean after yourself – a few words about Retention Policies</a><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>. These MVPs describe the feature in more depth.</p>

<hr />

<p>If you are curious what’s inside the <a href="https://github.com/StefanMaron/MSDyn365BC.Code.History/blob/master/BaseApp/Source/Base Application/RetentionPolicyJQ.Codeunit.al">Retention Policy JQ</a> you should check out the <a href="https://github.com/StefanMaron/MSDyn365BC.Code.History">amazing repository</a><sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup> provided by <a href="https://stefanmaron.com">Stefan Maron</a>.</p>

<p>Sometimes users are responsible and want to perform the action manually. In this case, the operation may fail due to insufficient permissions. Let’s have another look at the <a href="https://github.com/StefanMaron/MSDyn365BC.Code.History/blob/master/System%20Application/Source/System%20Application/Email/src/EmailLogsDelete.Codeunit.al">1598 “Email Logs Delete”</a> codeunit.</p>

<p>What we can see is a property <code class="language-plaintext highlighter-rouge">Permissions</code> on top of the object and an <code class="language-plaintext highlighter-rouge">[EventSubscriber(ObjectType::Codeunit, Codeunit::"Apply Retention Policy", 'OnApplyRetentionPolicyIndirectPermissionRequired'</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="p">...</span>

    <span class="n">Permissions</span> <span class="p">=</span> <span class="n">tabledata</span> <span class="s">"Demo Log"</span> <span class="p">=</span> <span class="n">rd</span><span class="p">;</span>

    <span class="p">...</span>

    <span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="n">Codeunit</span><span class="p">::</span><span class="s">"Apply Retention Policy"</span><span class="p">,</span> <span class="err">'</span><span class="n">OnApplyRetentionPolicyIndirectPermissionRequired</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">true</span><span class="p">,</span> <span class="k">true</span><span class="p">)]</span>
    <span class="n">local</span> <span class="n">procedure</span> <span class="nf">DeleteRecordsWithIndirectPermissionsOnApplyRetentionPolicyIndirectPermissionRequired</span><span class="p">(</span><span class="kt">var</span> <span class="n">RecRef</span><span class="p">:</span> <span class="n">RecordRef</span><span class="p">;</span> <span class="kt">var</span> <span class="n">Handled</span><span class="p">:</span> <span class="n">Boolean</span><span class="p">)</span>
    <span class="kt">var</span>
        <span class="n">RetentionPolicyLog</span><span class="p">:</span> <span class="n">Codeunit</span> <span class="s">"Retention Policy Log"</span><span class="p">;</span>
    <span class="n">begin</span>
        <span class="k">if</span> <span class="n">Handled</span> <span class="n">then</span>
            <span class="n">exit</span><span class="p">;</span>

        <span class="k">if</span> <span class="nf">not</span> <span class="p">(</span><span class="n">RecRef</span><span class="p">.</span><span class="n">Number</span> <span class="k">in</span> <span class="p">[</span><span class="n">Database</span><span class="p">::</span><span class="s">"Demo Log"</span><span class="p">])</span> <span class="n">then</span>
            <span class="n">exit</span><span class="p">;</span>

        <span class="c1">// if no filters have been set, something is wrong.</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">RecRef</span><span class="p">.</span><span class="nf">GetFilters</span><span class="p">()</span> <span class="p">=</span> <span class="err">''</span><span class="p">)</span> <span class="nf">and</span> <span class="p">(</span><span class="n">not</span> <span class="n">RecRef</span><span class="p">.</span><span class="nf">MarkedOnly</span><span class="p">())</span> <span class="n">then</span>
            <span class="n">RetentionPolicyLog</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="n">Enum</span><span class="p">::</span><span class="s">"Retention Policy Log Category"</span><span class="p">::</span><span class="s">"Retention Policy - Apply"</span><span class="p">,</span> <span class="nf">StrSubstNo</span><span class="p">(</span><span class="n">NoFiltersErr</span><span class="p">,</span> <span class="n">RecRef</span><span class="p">.</span><span class="n">Number</span><span class="p">,</span> <span class="n">RecRef</span><span class="p">.</span><span class="n">Name</span><span class="p">));</span>

        <span class="n">RecRef</span><span class="p">.</span><span class="nf">DeleteAll</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
        <span class="n">Handled</span> <span class="p">:=</span> <span class="k">true</span><span class="p">;</span>
    <span class="n">end</span><span class="p">;</span>

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

<p>Notice that we create an error in the <strong>Retention Policy Log</strong> whenever no filters have been set.</p>

<p>This was a brief overview of the <em>Retention Policy</em> feature in Dynamics 365 Business Central.</p>

<p>Hope you learned something new.</p>

<p>See you around 🦦</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365-release-plan/2020wave2/smb/dynamics365-business-central/planned-features#application">https://learn.microsoft.com/en-us/dynamics365-release-plan/2020wave2/smb/dynamics365-business-central/planned-features#application</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365/business-central/admin-data-retention-policies">https://learn.microsoft.com/en-us/dynamics365/business-central/admin-data-retention-policies</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://demiliani.com/2020/11/18/dynamics-365-business-central-creating-retention-policies-from-al/">https://demiliani.com/2020/11/18/dynamics-365-business-central-creating-retention-policies-from-al/</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://www.mynavblog.com/2020/12/21/clean-after-yourself-a-few-words-about-retention-policies/">https://www.mynavblog.com/2020/12/21/clean-after-yourself-a-few-words-about-retention-policies/</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p><a href="https://github.com/StefanMaron/MSDyn365BC.Code.History">https://github.com/StefanMaron/MSDyn365BC.Code.History</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlich</name></author><category term="BusinessCentral" /><category term="AL" /><category term="Retention Policy" /><summary type="html"><![CDATA[Allow your customers to purge non-critical data tables with the retention policy.]]></summary></entry><entry><title type="html">After initialization is before initialization</title><link href="https://christianbraeunlich.com/businesscentral/2022-09-27/system-initialization/" rel="alternate" type="text/html" title="After initialization is before initialization" /><published>2022-09-27T00:00:00+00:00</published><updated>2023-01-02T19:30:00+00:00</updated><id>https://christianbraeunlich.com/businesscentral/2022-09-27/system-initialization</id><content type="html" xml:base="https://christianbraeunlich.com/businesscentral/2022-09-27/system-initialization/"><![CDATA[<p>Hello there 👋</p>

<p>Recently, I’ve come across a rarely used event called <strong>OnCompanyInitialize</strong> which is published by the <em>Company-Initialize</em> codeunit. In my case, I wanted to create a new evaluation company and found that some important configurations were missing in custom modules.</p>

<p>So whenever creating a new company, the event is triggered. This gives us the opportunity to initialize our modules with some pre-configurations for newly created companies.</p>

<p class="notice--info">Remember that the customer expects the extension to work the same way when creating additional companies as it did before.</p>

<p>Also notice that this is the recommended way by Microsoft: <em>»If you are inserting data for a newly created company, we recommend subscribing to OnCompanyInitialize from Codeunit 2 instead.«</em><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>Let’s take a look at an example:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">codeunit</span> <span class="mi">56780</span> <span class="s">"CB Install"</span>
<span class="p">{</span>
  <span class="n">Subtype</span> <span class="o">=</span> <span class="n">Install</span><span class="p">;</span>

  <span class="n">trigger</span> <span class="nf">OnInstallAppPerCompany</span><span class="p">()</span>
  <span class="n">begin</span>
    <span class="nf">ModuleInitialization</span><span class="p">();</span>
  <span class="n">end</span><span class="p">;</span>

  <span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="nn">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="nn">Codeunit</span><span class="p">::</span><span class="s">"Company-Initialize"</span><span class="p">,</span> <span class="nv">'OnCompanyInitialize</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">false</span><span class="p">)]</span>
  <span class="n">local</span> <span class="n">procedure</span> <span class="nf">ModuleInitialization</span><span class="p">()</span>
  <span class="n">var</span>
      <span class="n">ModuleSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"Module Setup"</span><span class="p">;</span>
  <span class="n">begin</span>
      <span class="k">if</span> <span class="n">not</span> <span class="n">ModuleSetup</span><span class="nf">.Get</span><span class="p">()</span> <span class="n">then</span> <span class="n">begin</span>
          <span class="n">ModuleSetup</span><span class="nf">.Init</span><span class="p">();</span>
          <span class="n">ModuleSetup</span><span class="nf">.Insert</span><span class="p">();</span>
      <span class="n">end</span><span class="p">;</span>
  <span class="n">end</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This way you don’t have to worry about an uninitialized setup record. All the additional initialization steps are bundled into the <em>CompanyInitialize</em> procedure, which is triggered by the <em>OnCompanyInitialize</em> EventSubscriber.</p>

<p>There is also a CodeCop rule AA0235<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> that checks if the <em>OnCompanyInitialize</em> EventSubscriber is added. They also noticed that many extensions were missing an <em>OnCompanyInitialize</em> subscription<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>, which again goes back to my issue from the beginning. I found out about this rule from this tweet: <a href="https://twitter.com/rvanbekkum/status/1598703510794674176">https://twitter.com/rvanbekkum/status/1598703510794674176</a></p>

<p>UPDATE (2023-01-02) - A question was asked: “What’s the difference between the events <em>OnAfterInitialization</em> and <em>OnCompanyInitialize</em>?”. Good question.</p>

<p>Recently the <em>OnAfterInitialization</em> event became obsolete and is replaced by <em>OnAfterLogin</em>. That is because the <em>OnAfterLogin</em> shares an awesome new feature called <strong>Isolated Event</strong><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup> which means that each subscriber is executed in its own transaction.</p>

<p>Now with that in mind, what’s the difference between this new event and the <em>OnCompanyInitialize</em> event? The answer hides inside the BaseApp codeunit <em>CompanyInitialize</em>. So, let’s take a look:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">EventSubscriber</span><span class="p">(</span><span class="n">ObjectType</span><span class="p">::</span><span class="n">Codeunit</span><span class="p">,</span> <span class="n">Codeunit</span><span class="p">::</span><span class="s">"System Initialization"</span><span class="p">,</span> <span class="err">'</span><span class="n">OnAfterLogin</span><span class="err">'</span><span class="p">,</span> <span class="err">''</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">false</span><span class="p">)]</span>
<span class="n">local</span> <span class="n">procedure</span> <span class="nf">CompanyInitializeOnAfterLogin</span><span class="p">()</span>
<span class="kt">var</span>
    <span class="n">ClientTypeManagement</span><span class="p">:</span> <span class="n">Codeunit</span> <span class="s">"Client Type Management"</span><span class="p">;</span>
<span class="n">begin</span>
    <span class="p">...</span>
    <span class="nf">InitializeCompany</span><span class="p">();</span>
<span class="n">end</span><span class="p">;</span>
</code></pre></div></div>

<p>For a better overview I cut out a few lines. It’s just important to know that the <em>InitializeCompany</em> procedure is called at the end of the <strong>OnAfterLogin</strong> EventSubscriber.</p>

<p>This internal procedure now calls the <em>OnRun</em> trigger of the <em>Company-Initialize</em> codeunit:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="n">procedure</span> <span class="nf">InitializeCompany</span><span class="p">()</span>
<span class="kt">var</span>
    <span class="n">GLSetup</span><span class="p">:</span> <span class="n">Record</span> <span class="s">"General Ledger Setup"</span><span class="p">;</span>
<span class="n">begin</span>
    <span class="k">if</span> <span class="n">not</span> <span class="n">GLSetup</span><span class="p">.</span><span class="nf">Get</span><span class="p">()</span> <span class="n">then</span>
        <span class="n">CODEUNIT</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="n">CODEUNIT</span><span class="p">::</span><span class="s">"Company-Initialize"</span><span class="p">);</span>
<span class="n">end</span><span class="p">;</span>
</code></pre></div></div>

<p>So in the end, the <strong>OnCompanyInitialize</strong> event gets published after the setup procedures in the <em>OnRun</em> trigger (also cut out some lines for a better overview).</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">trigger</span> <span class="nf">OnRun</span><span class="p">()</span>
    <span class="n">begin</span>
        <span class="n">Window</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="n">Text000</span><span class="p">);</span>

        <span class="nf">OnBeforeOnRun</span><span class="p">();</span>

        <span class="nf">InitSetupTables</span><span class="p">();</span>
        <span class="n">AddOnIntegrMgt</span><span class="p">.</span><span class="nf">InitMfgSetup</span><span class="p">();</span>
        <span class="nf">InitSourceCodeSetup</span><span class="p">();</span>
        <span class="p">...</span>

        <span class="nf">OnCompanyInitialize</span><span class="p">();</span>

        <span class="n">Window</span><span class="p">.</span><span class="nf">Close</span><span class="p">();</span>

        <span class="nf">Commit</span><span class="p">();</span>
    <span class="n">end</span><span class="p">;</span>
</code></pre></div></div>

<p>With all this in mind, <strong>Isolated Event</strong> is a great improvement to the overall sustainability of the product. With a wide range of possible extensions from the marketplace<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup> to install, we want everything to run as stable as possible without affecting other non-dependent extensions. Otherwise, incidents may occure and users may not be able to sign-in again.</p>

<p>Hope you learned something new.</p>

<p>See you around 🦦</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/compliance/apptest-onbeforecompanyopen">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/compliance/apptest-onbeforecompanyopen</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0235">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0235</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0235#reason-for-the-rule">https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0235#reason-for-the-rule</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p><a href="https://learn.microsoft.com/en-us/dynamics365-release-plan/2022wave1/smb/dynamics365-business-central/isolated-events">https://learn.microsoft.com/en-us/dynamics365-release-plan/2022wave1/smb/dynamics365-business-central/isolated-events</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p><a href="https://appsource.microsoft.com/de-de/marketplace/apps?page=1&amp;product=dynamics-365-business-central">https://appsource.microsoft.com/de-de/marketplace/apps?page=1&amp;product=dynamics-365-business-central</a> <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>christianbraeunlich</name></author><category term="BusinessCentral" /><category term="AL" /><category term="Onboarding" /><summary type="html"><![CDATA[Initialize your extension when creating a new company.]]></summary></entry></feed>