<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Alex Shirokikh's Blog]]></title><description><![CDATA[A blog about Microsoft Dynamics 365 Business Central and the tech around it.]]></description><link>https://ashirokikh.com/</link><image><url>https://ashirokikh.com/favicon.png</url><title>Alex Shirokikh&apos;s Blog</title><link>https://ashirokikh.com/</link></image><generator>Ghost 5.88</generator><lastBuildDate>Wed, 04 Feb 2026 07:27:56 GMT</lastBuildDate><atom:link href="https://ashirokikh.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Inverting Dependencies with Interfaces in Business Central Application Language]]></title><description><![CDATA[How to effectively use interfaces in AL and manage your Business Central application source code better.]]></description><link>https://ashirokikh.com/inverting-dependencies-with-interfaces-in-business-central-application-language/</link><guid isPermaLink="false">6511539dbf7db36809bd4cf5</guid><category><![CDATA[AL]]></category><category><![CDATA[Business Central]]></category><category><![CDATA[Walkthrough]]></category><dc:creator><![CDATA[Alex Shirokikh]]></dc:creator><pubDate>Tue, 26 Sep 2023 09:36:58 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1638423589897-5e12c60da355?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDM0fHxwbHVnJTIwaW58ZW58MHx8fHwxNjk1NzE5MTY3fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1638423589897-5e12c60da355?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDM0fHxwbHVnJTIwaW58ZW58MHx8fHwxNjk1NzE5MTY3fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Inverting Dependencies with Interfaces in Business Central Application Language"><p>Although interfaces were introduced in Business Central 2020 release wave 1, it can still be a fairly new concept for Business Central (BC) developers who have not programmed in other language such as C#, TypeScript, or Java. Interfaces are not new in the software engineering world, and it&#x2019;s never too late to catch up with the rest of the industry on the good practices of writing and maintaining source code, and in particular, programming against an interface, not an implementation. Let&#x2019;s have a look at an evolution of a simplistic piece of software to demonstrate the benefits of using interfaces.</p><p><em>Disclaimer: the names of companies and providers used in this blog post except Microsoft are fictitious. Any similarity to the names is entirely coincidental.</em></p><h2 id="setting-the-stage">Setting the stage</h2><p>To find a theme for our simple solution written in Application Language (AL), we&#x2019;ll refer to the trendy tool the companies are implementing these days - AI. Let&#x2019;s imagine, our business leaders asked us to create a Business Central application for helping with some users&apos; questions, and we&#x2019;re the developers from the past without interfaces in AL. We (at least I) have no idea how to build proper prompts to real AI services, and although it won&#x2019;t often happen in real life, our manager agreed with my suggestion that calling real services of AI providers is out of scope of our solution. We decided to hardcode the responses from the services. Although everyone feels like this implementation may not bring any value to the business and could be a complete waste of time, the product should be enough to see the power of adding interfaces to your software development tool set. That&#x2019;s the real purpose of the project - learning something new!</p><p>We&#x2019;ll start with a simple AL application containing a page, an element to enter a question from a user, and an action button to get a response from the fake AI services. The page code below will be enough to get first development results.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">page 50100 &quot;ASH Fake AI Service&quot;
{
    ApplicationArea = Basic, Suite;
    Caption = &apos;Fake AI Service&apos;;
    DeleteAllowed = false;
    InsertAllowed = false;
    PageType = Card;
    UsageCategory = Tasks;

    layout
    {
        area(content)
        {
            field(Question; Question)
            {
                ApplicationArea = All;
                Caption = &apos;Question&apos;;
                MultiLine = true;
                ToolTip = &apos;The question to ask the AI service.&apos;;
            }
        }
    }

    actions
    {
        area(Processing)
        {
            action(&quot;Get AI Answer&quot;)
            {
                Caption = &apos;Get AI Answer&apos;;
                ApplicationArea = All;
                Image = Answers;
                ToolTip = &apos;Get the answer from the AI service.&apos;;
                Promoted = true;
                PromotedCategory = Process;
                PromotedIsBig = true;

                trigger OnAction()
                var
                    FakeAiServiceMgt: Codeunit &quot;ASH Fake AI Service Mgt.&quot;;
                begin
                    Message(FakeAiServiceMgt.GetAnswer(Question));
                end;
            }
        }
    }

    var
        Question: Text;

}</code></pre><figcaption>The beginning of page 50100 &quot;ASH Fake AI Service&quot;</figcaption></figure><p>And the codeunit provides an answer already, which is a good start.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot;
{
    procedure GetAnswer(Question: Text): Text
    begin
        if Question = &apos;&apos; then
            Error(&apos;Please enter your question.&apos;);

        exit(&apos;answer&apos;);
    end;
}
</code></pre><figcaption>The first version of codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot;</figcaption></figure><p>Our current solution is action should look like in the figure below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh5.googleusercontent.com/mD2MUOuGThOyY_IcdFyb_Knwp1sReLlJ11MCUXEl8etJwxywgO2D6Gg8UNem1eAjrhKUnAOkrwS2bEfPbpp0rYzH84MdvMHqyqZd07ovWxU_wFGLiEs2NIeBXqFtfpPoko8CVwCKIGL3S_Nn0-HRp6U" class="kg-image" alt="Inverting Dependencies with Interfaces in Business Central Application Language" loading="lazy" width="624" height="409"><figcaption>Fake AI Service page with a question and an answer</figcaption></figure><p>Let&#x2019;s call our first two fake AI service providers Bingle and Alfasoft. The system logic to call their services is a bit different and can change later, so we&#x2019;ll put them in dedicated codeunits and will call them from our main Fake AI Service Mgt. codeunit. To specify the provider, we&#x2019;ll create an extensible Enum object and add a new field with it to the page just above the Question element.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">enum 50100 &quot;ASH Fake AI Service Providers&quot;
{
    Extensible = true;

    value(50100; Bingle)
    {
    }
    value(50101; Alfasoft)
    {
    }
}</code></pre><figcaption>First version of enum 50100 &quot;ASH Fake AI Service Providers&quot;</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-AL">page 50100 &quot;ASH Fake AI Service&quot;
{
    ApplicationArea = Basic, Suite;
    Caption = &apos;Fake AI Service&apos;;
    DeleteAllowed = false;
    InsertAllowed = false;
    PageType = Card;
    UsageCategory = Tasks;

    layout
    {
        area(content)
        {
            field(Provider; Provider)
            {
                ApplicationArea = All;
                Caption = &apos;Provider&apos;;
                ToolTip = &apos;The AI service provider to use.&apos;;
            }

            field(Question; Question)
            {
                ApplicationArea = All;
                Caption = &apos;Question&apos;;
                MultiLine = true;
                ToolTip = &apos;The question to ask the AI service.&apos;;
            }
        }
    }

    actions
    {
        area(Processing)
        {
            action(&quot;Get AI Answer&quot;)
            {
                Caption = &apos;Get AI Answer&apos;;
                ApplicationArea = All;
                Image = Answers;
                ToolTip = &apos;Get the answer from the AI service.&apos;;
                Promoted = true;
                PromotedCategory = Process;
                PromotedIsBig = true;

                trigger OnAction()
                var
                    FakeAiServiceMgt: Codeunit &quot;ASH Fake AI Service Mgt.&quot;;
                begin
                    Message(FakeAiServiceMgt.GetAnswer(Provider, Question));
                end;
            }
        }
    }

    var
        Question: Text;
        Provider: Enum &quot;ASH Fake AI Service Providers&quot;;
}</code></pre><figcaption>Providers element added to page 50100 &quot;ASH Fake AI Service&quot;</figcaption></figure><p>The main codeunit GetAnswer procedure now needs the Provider parameter so that depending on the enum value that we select on the page, we are able to get answers from the service providers.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot;
{
    procedure GetAnswer(Provider: Enum &quot;ASH Fake AI Service Providers&quot;; Question: Text) Answer: Text
    var
        AlfasoftAiServiceMgt: Codeunit &quot;ASH Alfasoft AI Service Mgt.&quot;;
        BingleAiServiceMgt: Codeunit &quot;ASH Bingle AI Service Mgt.&quot;;
    begin
        if Question = &apos;&apos; then
            Error(&apos;Please enter your question.&apos;);

        case Provider of
            Provider::Bingle:
                Answer := BingleAiServiceMgt.GetAnswer(Question);
            Provider::Alfasoft:
                Answer := AlfasoftAiServiceMgt.GetAnswer(Question);
        end;
    end;
}</code></pre><figcaption>Codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot; logic for different providers</figcaption></figure><p>The dedicated to service provider logic codeunits are listed below.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50101 &quot;ASH Alfasoft AI Service Mgt.&quot;
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := &apos;Alfasoft says: hmm, interesting question&apos;;
    end;
}


codeunit 50102 &quot;ASH Bingle AI Service Mgt.&quot;
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := &apos;Bingle says: you know it better&apos;;
    end;
}</code></pre><figcaption>Answer logic for Bingle and Alfasoft providers</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh3.googleusercontent.com/lGNGsKWSA_eB3W37170yHtRXLLSmOCLSYXLd2kKunezMpKJroc6m19eZokPWZkk5mzpnrE-QFVlZIjZFGlLyEcVPBfxDP2fkYQ6A4IrRbq1hTOLoRTnJlt5j9oVpI25SaTH93C1oFIeJ87lj75XunD4" class="kg-image" alt="Inverting Dependencies with Interfaces in Business Central Application Language" loading="lazy" width="624" height="375"><figcaption>Service answers are now based on the specified provider</figcaption></figure><p>Notice how we must update the main Fake AI Service Mgt. codeunit every time we need to add a service provider. All the services implementations are explicitly referenced in the area where we defined our variables. This is called a tightly coupled code - changing in one codeunit often must be reflected in the other codeunit as well.</p><p>Now imagine that our service providers list keeps growing as well as related features and the main codeunit at some point becomes hard to read. The cases when one service was broken after making changes for another have started to pop up and eventually no one wants to touch that code anymore. The time it takes to ensure that nothing is broken starts to delay delivery of new application features.</p><p>To add fuel to the flame, a couple of our customers want to add their custom AI breakthrough solutions to the list as well but it sounds like we have no other choice apart from letting them customize our application for them and include their codeunits into our codebase. We don&#x2019;t believe it will be a good approach for maintaining and developing our solution further. Time to seriously think of a new way of organizing our code. Luckily for us, Microsoft introduced interfaces to Business Central Application Language, and we&#x2019;ve decided to explore this new platform feature.</p><h2 id="adding-an-interface">Adding an interface</h2><p>From the <a href="https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-interfaces-in-al?ref=ashirokikh.com">Microsoft Learn article about interfaces in AL</a> we understood that an interface is a syntactical contract which needs to be implemented. Let&#x2019;s have a look at our service providers code that we wrote for getting the answers. If we look at the service provider management codeunits as implementations with specific to provider details, what is the common function that a service provider codeunit has? That&#x2019;s right, the GetAnswer function. Our interface for fake AI service providers will look as follows.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">interface &quot;ASH AI Service Provider&quot;
{
    procedure GetAnswer(Question: Text) Answer: Text
}</code></pre><figcaption>Interface &quot;ASH AI Service Provider&quot;</figcaption></figure><p>The important part here is that the method signature - the procedure name, the parameters, and the returned value - must be exactly the same as we would expect it in our implementations. The visibility of a procedure will always be public there. We need interfaces only to define how our system will be communicating with an implementation. We won&#x2019;t be able to see private methods anyway.</p><p>Our providers services codeunits have procedures with the method signatures from the interface and therefore we can say that they implement it. We can explicitly say so in the program code by adding the implements keyword.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50101 &quot;ASH Alfasoft AI Service Mgt.&quot; implements &quot;ASH AI Service Provider&quot;
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := &apos;Alfasoft says: hmm, interesting question&apos;;
    end;
}

codeunit 50102 &quot;ASH Bingle AI Service Mgt.&quot; implements &quot;ASH AI Service Provider&quot;
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := &apos;Bingle says: you know it better&apos;;
    end;
}</code></pre><figcaption>Alfasof and Bingle services codeunits implement the interface</figcaption></figure><p>After rewriting our main codeunit, the code looks as below.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot;
{
    procedure GetAnswer(Provider: Enum &quot;ASH Fake AI Service Providers&quot;; Question: Text) Answer: Text
    var
        AlfasoftAiServiceMgt: Codeunit &quot;ASH Alfasoft AI Service Mgt.&quot;;
        BingleAiServiceMgt: Codeunit &quot;ASH Bingle AI Service Mgt.&quot;;
        AIServiceProvider: Interface &quot;ASH AI Service Provider&quot;;
    begin
        if Question = &apos;&apos; then
            Error(&apos;Please enter your question.&apos;);

        case Provider of
            Provider::Bingle:
                AIServiceProvider := BingleAiServiceMgt;
            Provider::Alfasoft:
                AIServiceProvider := AlfasoftAiServiceMgt;
        end;

        Answer := AIServiceProvider.GetAnswer(Question);
    end;
}</code></pre><figcaption>Our main codeunit contains assignments of our implementations to the interface variable depending on the Provider</figcaption></figure><p>We have defined a new variable of type Interface and assigned an implementation codeunit to it depending on the Provider parameter. Once we have an implementation assigned, we can call the GetAnswer method. Notice that we do it using the interface variable which at the moment of calling the method has a reference to the relevant instance of the implementation codeunit. Can we use this for extending our solution for the customers? Exposing an event sounds like a good option, so let&#x2019;s do it!</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot;
{
    procedure GetAnswer(Provider: Enum &quot;ASH Fake AI Service Providers&quot;; Question: Text) Answer: Text
    var
        AlfasoftAiServiceMgt: Codeunit &quot;ASH Alfasoft AI Service Mgt.&quot;;
        BingleAiServiceMgt: Codeunit &quot;ASH Bingle AI Service Mgt.&quot;;
        AIServiceProvider: Interface &quot;ASH AI Service Provider&quot;;
    begin
        if Question = &apos;&apos; then
            Error(&apos;Please enter your question.&apos;);

        case Provider of
            Provider::Bingle:
                AIServiceProvider := BingleAiServiceMgt;
            Provider::Alfasoft:
                AIServiceProvider := AlfasoftAiServiceMgt;
            else
                OnBeforeGetAnswer(Provider, AIServiceProvider);
        end;
        
        Answer := AIServiceProvider.GetAnswer(Question);
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetAnswer(Provider: Enum &quot;ASH Fake AI Service Providers&quot;; var AIServiceProvider: Interface &quot;ASH AI Service Provider&quot;)
    begin
    end;

}</code></pre><figcaption>OnBeforeGetAnswer integration event added to the main codeunit</figcaption></figure><p>All our customers have to do now is to extend the enum 50100 &quot;ASH Fake AI Service Providers&quot; and subscribe to the new handy event where they can assign their implementations to the AIServiceProvider variable.</p><h2 id="creating-an-extension-to-the-main-app">Creating an extension to the main app</h2><p>To demonstrate that, let&#x2019;s create a separate app for the new fake AI service provider from the company called Apricot, and name the app Custom AI Provider. We want it to be automatically integrated into our Fake AI Service page once the app is installed.</p><p>After we have set up the dependency and the workspace for the two apps, we can define the new enum extension as follows.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">enumextension 50150 &quot;ASH Cust. AI Service Providers&quot; extends &quot;ASH Fake AI Service Providers&quot;
{
    value(50150; Apricot)
    {
    }
}</code></pre><figcaption>Enum extension of &quot;ASH Fake AI Service Providers&quot; containing Apricot value</figcaption></figure><p>The Apricot service provider codeunit needs its own interface implementation and we will complete the integration of the custom app via the event subscription.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50150 &quot;ASH Apricot AI Service Mgt.&quot; implements &quot;ASH AI Service Provider&quot;
{
    procedure GetAnswer(Question: Text) Answer: Text
    begin
        Answer := &apos;Apricot says: follow your heart&apos;;
    end;
}

codeunit 50151 &quot;ASH Cust. Serv. Provider Mgt.&quot;
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::&quot;ASH Fake AI Service Mgt.&quot;, OnBeforeGetAnswer, &apos;&apos;, false, false)]
    local procedure OnBeforeGetAnswer(Provider: Enum &quot;ASH Fake AI Service Providers&quot;; var AIServiceProvider: Interface &quot;ASH AI Service Provider&quot;);
    var
        ApricotAIServiceMgt: Codeunit &quot;ASH Apricot AI Service Mgt.&quot;;
    begin
        if Provider = Provider::Apricot then
            AIServiceProvider := ApricotAIServiceMgt;
    end;
}</code></pre><figcaption>Apricot AI Service Mgt codeunit and the event subscriber code</figcaption></figure><p>Once the second app is also published, the custom implementation can now be tested along with the built-in ones, and we get the expected response.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh4.googleusercontent.com/iKIi7txGGLpwWvzcQL2EK_k1TBsQWvsVDXVktogTBpxwxQUE0kReOt6OvnHNNx8Xm21mKbv5nmjoe6ajjxhWcd6H460Ro8nG99ntnBc8AIbvp1hsoak40MZ1qqayuY0C3KegtuOLU8icFyVc-G-CzY0" class="kg-image" alt="Inverting Dependencies with Interfaces in Business Central Application Language" loading="lazy" width="624" height="379"><figcaption>Apricot appears on the Provider list and the response comes from the custom app</figcaption></figure><p>Phew, a customized code base is no longer required and the future looks brighter. Our Customers are still looking forward to doing business together with us!</p><p>What we have done now is applying the Dependency Inversion principle, one of the SOLID principles of software design. Instead of making our base product code dependent and tightly coupled with the customers code, we let customers&#x2019; solutions to depend on ours and inverted the dependency. This approach allows us to develop our features without really having to know about the custom logic for service providers. All we needed to do is to let the custom solutions know what our product expects them to do - provide a GetAnswers method implementation - and that we did with the interface definition. All customers needed to do is to have implemented the interface. Everyone can do what they want now without interference as long as the system communication contract is fulfilled.</p><h2 id="mapping-interfaces-with-implementations">Mapping interfaces with implementations</h2><p>Although we&#x2019;ve sold a couple of program code management problems, our main codeunit &quot;ASH Fake AI Service Mgt.&quot; still needs updating when a service provider is added to our base product. There is a neater way to map interfaces with implementations that will let us completely free the codeunit from references to specific AI provider services codeunits and related to them logic leaving only reusable program code. We will look into it next.</p><p>The fact is that Enums in AL also can implement interfaces. Well, not exactly but with Enums we can further reference an implementation against each value. There is also the DefaultImplementation Enum property that can be used for all values without a specific Implementation in case we need it. Let&#x2019;s see how we can use the enum 50100 &quot;ASH Fake AI Service Providers&quot; more effectively.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">enum 50100 &quot;ASH Fake AI Service Providers&quot; implements &quot;ASH AI Service Provider&quot;
{
    Extensible = true;

    value(50100; Bingle)
    {
        Implementation = &quot;ASH AI Service Provider&quot; = &quot;ASH Bingle AI Service Mgt.&quot;;
    }
    value(50101; Alfasoft)
    {
        Implementation = &quot;ASH AI Service Provider&quot; = &quot;ASH Alfasoft AI Service Mgt.&quot;;
    }
}</code></pre><figcaption>Enum &quot;ASH Fake AI Service Providers&quot; includes &quot;ASH AI Service Provider&quot; interface implementations</figcaption></figure><p>With this approach, we can further clean up our main codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot; and remove all the implementation variables which we mapped in the Enum object.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot;
{
    procedure GetAnswer(Provider: Enum &quot;ASH Fake AI Service Providers&quot;; Question: Text) Answer: Text
    var
        AIServiceProvider: Interface &quot;ASH AI Service Provider&quot;;
    begin
        if Question = &apos;&apos; then
            Error(&apos;Please enter your question.&apos;);

        case Provider of
            Provider::Bingle, Provider::Alfasoft:
                AIServiceProvider := Provider;
            else
                OnBeforeGetAnswer(Provider, AIServiceProvider);
        end;

        Answer := AIServiceProvider.GetAnswer(Question);
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetAnswer(Provider: Enum &quot;ASH Fake AI Service Providers&quot;; var AIServiceProvider: Interface &quot;ASH AI Service Provider&quot;)
    begin
    end;
}</code></pre><figcaption>Main codeunit updated to assigned an enum value to interface variable</figcaption></figure><p>The dependent app Custom AI Provider has broken with the change - it also requires the enum extension update.</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">enumextension 50150 &quot;ASH Cust. AI Service Providers&quot; extends &quot;ASH Fake AI Service Providers&quot;
{
    value(50150; Apricot)
    {
        Implementation = &quot;ASH AI Service Provider&quot; = &quot;ASH Apricot AI Service Mgt.&quot;;
    }
}</code></pre><figcaption>Enum extension in Custom AI Provider also maps the implementation</figcaption></figure><p>With this change, we no longer need the event and the subscriber code, all implementations are now mapped via the &quot;ASH Fake AI Service Providers&quot; enum and we can remove the redundant program code - delete entire codeunit 50151 &quot;ASH Cust. Serv. Provider Mgt.&quot; and remove the OnBeforeGetAnswer event and related code from the codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot;. The main codeunit looks as lean as never before!</p><figure class="kg-card kg-code-card"><pre><code class="language-AL">codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot;
{
    procedure GetAnswer(Provider: Enum &quot;ASH Fake AI Service Providers&quot;; Question: Text) Answer: Text
    var
        AIServiceProvider: Interface &quot;ASH AI Service Provider&quot;;
    begin
        if Question = &apos;&apos; then
            Error(&apos;Please enter your question.&apos;);

        AIServiceProvider := Provider;
        Answer := AIServiceProvider.GetAnswer(Question);
    end;
}</code></pre><figcaption>Event has been removed from the main codeunit</figcaption></figure><p>There is actually no essential need for the main codeunit 50100 &quot;ASH Fake AI Service Mgt.&quot; now. We can assign the enum to the interface directly in the page action if you wish to go further.</p><p>At this point, we have rewritten both of the apps to use Enum as the implementation mapper. The main codeunit no longer needs to know about any implementation details of the service. All it is responsible from now on is only processing whatever we receive from the service calls. If we need to add a new service provider, we would only need to add it to the Enum object and provide the interface implementation. The service page and the main codeunit is now reused across entire functionality when we need to add new service providers or update their system logic.</p><h2 id="an-example-from-business-central-apps">An example from Business Central apps</h2><p>Every time I need to learn more about using AL types or find code samples, I tend to look into the standard Microsoft applications first. There is a great example for interfaces there - the Email Connector interface. It&#x2019;s defined in the System Application and implemented in dedicated to a particular connector apps, for instance, SMTP or Microsoft 365. The below is a screenshot from my environment.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh3.googleusercontent.com/QXe2JFWv5IFlDqTZIY5rTtp2wqZHKbSwZfR9o3kanvN13rkxDRXjLTv4CLkUbkY5QE6F5ePYM4XI8evu1avDrILsulux5_o_Txtp9ycFXcLQ5NHBLyvcyu1ALN1WbLTK3Ns9QSbGJmdqV6DO2KXnD68" class="kg-image" alt="Inverting Dependencies with Interfaces in Business Central Application Language" loading="lazy" width="624" height="224"><figcaption>Extension management page with listed Email apps from Microsoft</figcaption></figure><p>When we use the Set Up Email page, where do you think the list items come from?</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh3.googleusercontent.com/X3ZdEo6E7NnAvmT3BH8dRNFH1U1MHCpLi4HiKIPI15i2DKz-vE-e7wrdCC3QV1dAckwCJz0iPSw6Ncqq6iBHTOYE3QhBZ9a0r2KIkpUdlB6XnVZvtXr4hZBfYAvl1cHuqRpFGRWuYnRQ2ncfzPzGF24" class="kg-image" alt="Inverting Dependencies with Interfaces in Business Central Application Language" loading="lazy" width="581" height="544"><figcaption>Set Up Email page with Account Types listed</figcaption></figure><p>You&#x2019;re right, from the installed Email apps. By this time, you already know how those items automatically appear there, well done! If you look into the Business Central applications source code, I&#x2019;m sure you&#x2019;ll find something new that we haven&#x2019;t covered today.</p><h2 id="summary">Summary</h2><p>After making a couple of impractical decisions in the beginning of our AI services app design, I&#x2019;m glad that we have finally achieved the primary objective of our project. I hope that by gradually making changes to our source code, we all now have a clear understanding that the main purpose of interfaces is being able to separate implementation details from the system parts that need not be concerned with them. Especially, if we have several ways of performing the same task and we want to create a program module or an app for each of them.</p><p>We were able to enable our valuable customers to work on their own software modules extending our base product. We still could improve our application without interference due to loose coupling of our apps achieved by applying the Dependency Inversion principle. </p><p>With the interfaces concept understood and applied, we have also reduced the number of code lines and made our code more readable and maintainable. </p><p>If there are still questions on the subject or I missed anything along the way, please let me know. Happy coding!</p><p>The source code for this blog post can be found at <a href="https://github.com/ashirokikh/ashirokikh.com?ref=ashirokikh.com">https://github.com/ashirokikh/ashirokikh.com</a></p>]]></content:encoded></item><item><title><![CDATA[What's inside a Business Central Configuration Package]]></title><description><![CDATA[Tracking content of a Business Central configuration package can help to ensure the packaged data and its setup as per your expectation. Here is a way to do it with some code snippets for process automation.]]></description><link>https://ashirokikh.com/whats-inside-a-business-central-rapidstart-configuration-package/</link><guid isPermaLink="false">64d7561ebf7db36809bd4976</guid><category><![CDATA[Business Central]]></category><category><![CDATA[BcContainerHelper]]></category><dc:creator><![CDATA[Alex Shirokikh]]></dc:creator><pubDate>Sat, 12 Aug 2023 14:54:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1640158615573-cd28feb1bf4e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI3fHxib3glMjBkYXRhfGVufDB8fHx8MTY5MTgzNzg2Mnww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1640158615573-cd28feb1bf4e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI3fHxib3glMjBkYXRhfGVufDB8fHx8MTY5MTgzNzg2Mnww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="What&apos;s inside a Business Central Configuration Package"><p>The most common way of creating validation data for Business Central is entering the data manually and then creating a (RapidStart) Configuration Package to export the created data and import it into another application environment. There are many articles and blog posts on how to use configuration packages and this blog post goes beyond that.</p><p>Configuration packages are a powerful tool and the data import is a fairly simple process (depending on the setup, of course). There is one challenge I faced, however, while automating a Business Central environment creation - tracking configuration package content. After the package is exported, it&apos;s hard to say whether it has all what you would expect it to have. </p><h3 id="rapidstart-file-content">RapidStart file content</h3><p>If we look at the standard source code for package export, the data is processed as an XML document (codeunit 8614 &quot;Config. XML Exchange&quot;) and then compressed using the GZip format (codeunit 8619 &quot;Config. Pckg. Compression Mgt.&quot;). That means we should be able to change the .rapidstart extension of the exported file to .gz and use an archiver software like 7-Zip to get the content. In my case, the contained file did not have an extension, therefore, once the file is decompressed, the result file extension needed to be specified as .xml to open it with a default viewer. That&apos;s it, a configuration package is essentially an archived XML file changes of which we can track as any text file.</p><h3 id="showcase-scenario">Showcase scenario</h3><p>Let&apos;s do a simple exercise that will show how the changes can be reviewed. Imagine that we develop a Business Central application extension. To streamline demonstration of our extension to the end users, we need specific to our application data setup. We decided to use configuration packages to consistently recreate the required data set by importing that data on top of the standard Microsoft Cronus company validation (demo) data. One of the tables that we need our custom data for is Customer. Our initial configuration package setup might look as in the picture below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2023/08/image.png" class="kg-image" alt="What&apos;s inside a Business Central Configuration Package" loading="lazy" width="1130" height="891" srcset="https://ashirokikh.com/content/images/size/w600/2023/08/image.png 600w, https://ashirokikh.com/content/images/size/w1000/2023/08/image.png 1000w, https://ashirokikh.com/content/images/2023/08/image.png 1130w" sizes="(min-width: 720px) 720px"><figcaption>Configuration package after adding the Customer table</figcaption></figure><p>The exported and decompressed package file contains data from the listed in the configuration package tables including the setup such as information about included table filters and fields order as well as the data itself.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2023/08/image-1.png" class="kg-image" alt="What&apos;s inside a Business Central Configuration Package" loading="lazy" width="968" height="775" srcset="https://ashirokikh.com/content/images/size/w600/2023/08/image-1.png 600w, https://ashirokikh.com/content/images/2023/08/image-1.png 968w" sizes="(min-width: 720px) 720px"><figcaption>Example of XML file content with data of two Customers</figcaption></figure><p>To exclude the standard Cronus data, our extension-specific Customer data is filtered by the No. field with the &quot;TEST*&quot; filter value, and we can see it under the ConfigPackageFilterList element of the XML package file.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2023/08/image-2.png" class="kg-image" alt="What&apos;s inside a Business Central Configuration Package" loading="lazy" width="843" height="196" srcset="https://ashirokikh.com/content/images/size/w600/2023/08/image-2.png 600w, https://ashirokikh.com/content/images/2023/08/image-2.png 843w" sizes="(min-width: 720px) 720px"><figcaption>Configuration package Customers FIeldFIlter XML value is TEST*</figcaption></figure><p>We&apos;re happy with the data for now and we commit this file version along with the source code of the extension.</p><p>With our next useful feature, we added a custom Customer field (for this example, let&apos;s call it &quot;ASH Important Field&quot;), and would like to include that data into our package. A new Cronus company environment is created from scratch as usual to avoid data and configuration drift, our extension published to it and the configuration package is archived back to GZip format, imported and applied. Now we need to update the field values, add the field into the configuration package setup, and export the rapidstart file. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2023/08/image-3.png" class="kg-image" alt="What&apos;s inside a Business Central Configuration Package" loading="lazy" width="928" height="573" srcset="https://ashirokikh.com/content/images/size/w600/2023/08/image-3.png 600w, https://ashirokikh.com/content/images/2023/08/image-3.png 928w" sizes="(min-width: 720px) 720px"><figcaption>ASH Important Field is updated on the Test Customer Card</figcaption></figure><p>After going through the XML extraction process, we can clearly see the changes that a field has been added with the data from the database. We have confirmed nothing else is lost and only the expected data update is going to be committed to the source code repository. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2023/08/image-4.png" class="kg-image" alt="What&apos;s inside a Business Central Configuration Package" loading="lazy" width="1065" height="721" srcset="https://ashirokikh.com/content/images/size/w600/2023/08/image-4.png 600w, https://ashirokikh.com/content/images/size/w1000/2023/08/image-4.png 1000w, https://ashirokikh.com/content/images/2023/08/image-4.png 1065w" sizes="(min-width: 720px) 720px"><figcaption>XML file comparison in VS Code with the new ASHImportantField values</figcaption></figure><p>By following this approach, every time we change the source code and the data needs an update, we track all data changes as well so that at all times the version of the application source code corresponds with the data version that we need our code to be demoed with. </p><h3 id="automating-the-process">Automating the process</h3><p>The conversion from rapidstart to xml and back can be done with a PowerShell script. The below are the functions that can perform that.</p><figure class="kg-card kg-code-card"><pre><code class="language-powershell">function Convert-RapidStartFileToXmlFile {
    param(
        [Parameter(Mandatory=$true)]
        [string]$RapidStartFilePath
    )
    $filePath = Join-Path $PSScriptRoot ([System.IO.Path]::GetFileName($RapidStartFilePath)) -Resolve
    $XmlFilePath = [System.IO.Path]::ChangeExtension($filePath, &quot;xml&quot;)
    Write-Host &quot;Decompressing $filePath to $XmlFilePath&quot;
    $xmlFileStream = New-Object IO.FileStream ($XmlFilePath, [System.IO.FileMode]::Create)
    $rapidstartFileStream = New-Object IO.FileStream ($filePath, [System.IO.FileMode]::Open, [IO.FileAccess]::Read)
    $gzipStream = New-Object IO.Compression.GzipStream ($rapidstartFileStream, [IO.Compression.CompressionMode]::Decompress)
    
    try {
        $gzipStream.CopyTo($xmlFileStream)
    }
    finally {
        $gzipStream.Close()
        $rapidstartFileStream.Close()
        $xmlFileStream.Close()
    }
}

function Convert-XmlFileToRapidStartFile {
    param(
        [Parameter(Mandatory=$true)]
        [string]$XmlFilePath
    )
    $filePath = Join-Path $PSScriptRoot ([System.IO.Path]::GetFileName($XmlFilePath)) -Resolve
    $RapidStartFilePath = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetFullPath($filePath), &quot;rapidstart&quot;)
    Write-Host &quot;Compressing $filePath to $RapidStartFilePath&quot;
    $xmlFileStream = New-Object IO.FileStream ($filePath, [IO.FileMode]::Open, [IO.FileAccess]::Read)
    $rapidstartFileStream = New-Object IO.FileStream ($RapidStartFilePath, [System.IO.FileMode]::Create)
    $gzipStream = New-Object IO.Compression.GzipStream ($rapidstartFileStream, [IO.Compression.CompressionMode]::Compress)
    
    try {
        $xmlFileStream.CopyTo($gzipStream)
    }
    finally {
        $xmlFileStream.Close()
        $gzipStream.Close()
        $rapidstartFileStream.Close()        
    }
}</code></pre><figcaption>PowerShell functions to extract XML file from a RapidStart file and to compress it back</figcaption></figure><p> The below are examples of how those functions can be called.</p><figure class="kg-card kg-code-card"><pre><code class="language-powershell"># Example of getting the contents of a RapidStart file:
Convert-RapidStartFileToXmlFile -RapidStartFilePath .\PackageCHECK-CONTENTS.rapidstart

# Example of creating a RapidStart file from an xml file:
Convert-XmlFileToRapidStartFile -XmlFilePath .\PackageCHECK-CONTENTS.xml</code></pre><figcaption>Calling the Convert-RapidStartFileToXmlFile and Convert-XmlFileToRapidStartFile functions</figcaption></figure><p>After the XML file that we track changes for is converted to a rapidstart file, it can be imported back into a Business Central environment. There is no need to track the original rapidstart file as we always can create it from the XML file.</p><p>The PowerShell scripts are helpful in particular if we use some kind of continuous integration (CI) pipeline which uses Docker containers to run automated tests. For example, if your source code repository is set up with AL-Go for GitHub, the ImportTestDataInBcContainer can be customized so that the <code>Convert-XmlFileToRapidStartFile</code> function is run to create the rapidstart file and then the <code>UploadImportAndApply-ConfigPackageInBcContainer</code> is called with the created file path as a parameter. </p><h3 id="summary">Summary</h3><p>We have found out that a Business Central configuration package is an archived XML file and we can track its history of modifications and still be able to import it back to a newly created environment either manually or as part of an automated process using PowerShell scripts. The downside of this approach that needs to be considered is that the extracted XML file is significantly bigger than the archived configuration package. </p><p>The source code for this blog post can be found at <a href="https://github.com/ashirokikh/ashirokikh.com?ref=ashirokikh.com">https://github.com/ashirokikh/ashirokikh.com</a></p>]]></content:encoded></item><item><title><![CDATA[Navigating Microsoft Base and System Apps with VS Code Workspaces]]></title><description><![CDATA[Set up an environment where Go to References works across your AL extensions as well as Base and System Applications of Business Central.]]></description><link>https://ashirokikh.com/navigating-microsoft-base-and-system-applications-with-vs-code-workspaces/</link><guid isPermaLink="false">61a5b8e2d9c7127cf13ddde4</guid><category><![CDATA[AL]]></category><category><![CDATA[Walkthrough]]></category><category><![CDATA[Business Central]]></category><dc:creator><![CDATA[Alex Shirokikh]]></dc:creator><pubDate>Sat, 04 Dec 2021 11:00:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1484910292437-025e5d13ce87?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDV8fG5hdmlnYXRlfGVufDB8fHx8MTYzODI1MDk1NQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1484910292437-025e5d13ce87?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDV8fG5hdmlnYXRlfGVufDB8fHx8MTYzODI1MDk1NQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces"><p>There are good old tools for finding references to C/AL table fields or methods such as <a href="https://www.stati-cal.com/PrismForCAL/?ref=ashirokikh.com">Prism for C/AL</a>, or <a href="https://mibuso.com/downloads/gdt-where-used-tool?ref=ashirokikh.com">GDT Where Used Tool</a> which I find very useful even today. Customizing the standard Dynamics NAV application used to be the most common way of filling the gaps in the business processes implementation, and these tools were absolutely crucial to have in order to make the modification reliable and consistent across the entire application. The <code>Go to Definition</code> action was added to C/SIDE some time ago, but there is still no big help in finding all places where an object is used. </p><p>With the move to AL, VS Code became the main development environment. It has been widely used by software developers to write applications in almost any language, and therefore, it has all the necessary tools to make their daily work productive. As a Business Central developer, there is one feature of VS Code that I wish the AL Language extension had. When working with an extension, it would be very helpful if the <code>Go to References</code> action supported finding code references across the extension <strong>and</strong> the AL symbols. </p><h2 id="challenge">Challenge</h2><p>Let&apos;s imagine we have just run the <code>AL: Go!</code> command and created a new extension project. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-1.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="1034" height="708" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-1.png 600w, https://ashirokikh.com/content/images/size/w1000/2021/11/image-1.png 1000w, https://ashirokikh.com/content/images/2021/11/image-1.png 1034w" sizes="(min-width: 720px) 720px"><figcaption>New AL Project</figcaption></figure><p>We have a task that involves extending an object. Getting familiar with the object and its references first is a good idea before we can say how big our work is. For that, we press <code>Ctrl</code> and left-click on the &quot;Customer List&quot; to go to definition. The <code>SourceTable</code> of the page is Customer, and we&apos;d like to see where it&apos;s used. We right-click on <code>Customer</code> and select the Go to References action. The figure below shows the not very exciting message saying that <code>No references found for &apos;Customer&apos;</code>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-3.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="1034" height="708" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-3.png 600w, https://ashirokikh.com/content/images/size/w1000/2021/11/image-3.png 1000w, https://ashirokikh.com/content/images/2021/11/image-3.png 1034w" sizes="(min-width: 720px) 720px"><figcaption>Finding references across symbols not supported</figcaption></figure><p>Of course, we can take the source code from the Business Central installation archive, extract it into a folder, and open it in VS Code as a second window to explore. And we may need another window if we need to dig further into the System Application to see examples of the <a href="https://alguidelines.dev/bcpatterns/facade-pattern/?ref=ashirokikh.com">Facade design pattern</a>, for example, when we get bored. Wouldn&apos;t it be nice to have everything in one place? I hope it will be working sometime in the future but for now, let&apos;s see what we can do about it by using VS Code Workspaces.</p><h2 id="adding-projects-to-vs-code-workspace">Adding projects to VS Code workspace</h2><p>A folder opened by VS Code is essentially a workspace. We&apos;re going to put together several folders, which will become a <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-multiroot-workspaces?ref=ashirokikh.com">multi-root workspace</a>. It&apos;s supported by the AL Language extension, so let&apos;s start by creating an empty folder and opening it. Then, we follow the menu <code>File -&gt; Add Folder to Workspace...</code> and select the ALProject1 folder with the extension project we created earlier. The result now should look as follows.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-6.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="754" height="571" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-6.png 600w, https://ashirokikh.com/content/images/2021/11/image-6.png 754w" sizes="(min-width: 720px) 720px"><figcaption>Workspace created</figcaption></figure><p>Let&apos;s keep this workspace for future use and go to <code>File -&gt; Save Workspace As...</code>, which will create a new code-workspace file. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-11.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="941" height="712" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-11.png 600w, https://ashirokikh.com/content/images/2021/11/image-11.png 941w" sizes="(min-width: 720px) 720px"><figcaption>Workspace saved</figcaption></figure><p>Your folder path might be different depending on your projects&apos; locations. </p><p>If you want to keep just one set of symbols in the workspace, and this is a completely optional step, create an <code>alpackages</code> folder where we&apos;ll have our symbols to share across the projects of our workspace. By adding the <code>al.packageCachePath</code> path to the workspace settings, we let the AL compiler know where to look for the symbols. In my case, the path to the <code>alpackages</code> folder is <code>C:\al\al-workspace\alpackages</code>. It&apos;s important to have an absolute path so that all referenced projects refer to the same path. </p><p>After that, copy the symbols from our <code>ALProject1\.alpackages</code> folder to the workspace&apos;s package cache folder and reload VS Code (hit <code>Ctrl+Shift+P</code> to open Command Palette and type <code>reload</code> to find the <code>Developer: Reload Window</code> command) to finish the symbols setup. All red squiggly lines should go away now and our ALProject1 should be able to see the shared symbols. The window now should look as in the below figure.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-12.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="1185" height="712" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-12.png 600w, https://ashirokikh.com/content/images/size/w1000/2021/11/image-12.png 1000w, https://ashirokikh.com/content/images/2021/11/image-12.png 1185w" sizes="(min-width: 720px) 720px"><figcaption>Workspace packages cache path and symbols setup</figcaption></figure><p><strong>Update 28/04/2022</strong>: Although knowing about the <code>al.packageCachePath</code> setting can be useful, symbols also can be downloaded for each of the added app separately, if we skip the previous step. The AL compiler seems to like symbols per app more.</p><p>Now things get even more interesting. Let&apos;s add the Base App source code. </p><p>The standard Business Central source code comes with the installation package. For this exercise, I&apos;m following a tip from <a href="https://www.kauffmann.nl/?ref=ashirokikh.com">Arend-Jan Kauffmann</a> given in a <a href="http://bit.ly/BCProfDiscord?ref=ashirokikh.com">BC Professionals Discord channel</a>. I&apos;m going to <a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository?ref=ashirokikh.com">clone</a> <a href="https://stefanmaron.com/?ref=ashirokikh.com">Stefan Maro&#x144;</a>&apos;s repository <a href="https://github.com/StefanMaron/MSDyn365BC.Code.History?ref=ashirokikh.com">MSDyn365BC.Code.History</a>. It is regularly updated when a major version is released. Its current size is more than 5 GB, which means cloning the repository will take some time. The fetched codebase can be very handy if we want to play with different localization or versions, we could just check out the dedicated branch. </p><p>I&apos;m working with the Australian localization, therefore, once the repo is cloned, I check out the <code>au-19</code> branch and add the Base App source folder (in my case, at <code>C:\git\MSDyn365BC.Code.History\BaseApp\Source\Base Application</code>) to our workspace. VS Code will process the new project addition for several minutes, there should be no errors as a result. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-14.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="1051" height="776" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-14.png 600w, https://ashirokikh.com/content/images/size/w1000/2021/11/image-14.png 1000w, https://ashirokikh.com/content/images/2021/11/image-14.png 1051w" sizes="(min-width: 720px) 720px"><figcaption>Base App added to workspace</figcaption></figure><p>If we try and Go to Definition of the <code>Customer List</code> page now, we&apos;ll end up on the <code>Customer List.dal</code> tab with the symbols source code as before. The trick of pointing to the Base App project in our workspace is adding the Base App as a dependency to the extension app. After updating our extension, the <code>app.json</code> file content will look as follows.</p><figure class="kg-card kg-code-card"><pre><code class="language-json">{
  &quot;id&quot;: &quot;583af059-c0cb-42e2-b2d8-eeb89e3ad67a&quot;,
  &quot;name&quot;: &quot;ALProject1&quot;,
  &quot;publisher&quot;: &quot;Default publisher&quot;,
  &quot;version&quot;: &quot;1.0.0.0&quot;,
  &quot;brief&quot;: &quot;&quot;,
  &quot;description&quot;: &quot;&quot;,
  &quot;privacyStatement&quot;: &quot;&quot;,
  &quot;EULA&quot;: &quot;&quot;,
  &quot;help&quot;: &quot;&quot;,
  &quot;url&quot;: &quot;&quot;,
  &quot;logo&quot;: &quot;&quot;,
  &quot;dependencies&quot;: [
    {
      &quot;id&quot;: &quot;437dbf0e-84ff-417a-965d-ed2bb9650972&quot;,
      &quot;name&quot;:  &quot;Base Application&quot;,
      &quot;publisher&quot;:  &quot;Microsoft&quot;,
      &quot;version&quot;: &quot;19.1.0.0&quot;
    }
  ],
  &quot;screenshots&quot;: [],
  &quot;platform&quot;: &quot;1.0.0.0&quot;,
  &quot;application&quot;: &quot;19.0.0.0&quot;,
  &quot;idRanges&quot;: [
    {
      &quot;from&quot;: 50100,
      &quot;to&quot;: 50149
    }
  ],
  &quot;resourceExposurePolicy&quot;: {
    &quot;allowDebugging&quot;: true,
    &quot;allowDownloadingSource&quot;: false,
    &quot;includeSourceInSymbolFile&quot;: false
  },
  &quot;runtime&quot;: &quot;8.0&quot;
}</code></pre><figcaption>app.json with Base App as dependency</figcaption></figure><p>This is it! Now if we go to the Customer List page definition from our HelloWorld.al code, we&apos;ll get into the <code>CustomerList.Page.al</code> file! Now try using Go to Referfeces from the <code>Customer List</code> page reference in the page object. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-13.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="997" height="432" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-13.png 600w, https://ashirokikh.com/content/images/2021/11/image-13.png 997w" sizes="(min-width: 720px) 720px"><figcaption>References across workspaces</figcaption></figure><p>Right? There are references from our extension and Base Application. Now we&apos;re navigating! </p><p>If Base Application navigation is all you need, you could stop at this point. Although I&apos;d like to say that System Application is something that we definitely need to check out. It&apos;s just beautiful, implemented as separate apps using interfaces. I&apos;ll add my <code>C:\git\MSDyn365BC.Code.History\System Application\Source\System Application</code> folder to the workspace and give it some time to cook. </p><p>Now let&apos;s imagine, we need to work with InStream or OutStream, the Temp Blob codeunit can help with that.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-19.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="1051" height="709" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-19.png 600w, https://ashirokikh.com/content/images/size/w1000/2021/11/image-19.png 1000w, https://ashirokikh.com/content/images/2021/11/image-19.png 1051w" sizes="(min-width: 720px) 720px"><figcaption>Temp Blob codeunit referenced</figcaption></figure><p>The Temp Blob codeunit is part of System Application. We get to the symbols code if we navigate to its definition at this point. It should be okay after updating our <code>app.json</code> again by adding the following dependency after a comma.</p><figure class="kg-card kg-code-card"><pre><code class="language-json"> {
   &quot;id&quot;:  &quot;63ca2fa4-4f03-4f2b-a480-172fef340d3f&quot;,
   &quot;name&quot;:  &quot;System Application&quot;,
   &quot;publisher&quot;:  &quot;Microsoft&quot;,
   &quot;version&quot;:  &quot;19.1.0.0&quot;
 }</code></pre><figcaption>System Application dependency</figcaption></figure><p>The result should look as in the figure below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-18.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="1051" height="709" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-18.png 600w, https://ashirokikh.com/content/images/size/w1000/2021/11/image-18.png 1000w, https://ashirokikh.com/content/images/2021/11/image-18.png 1051w" sizes="(min-width: 720px) 720px"><figcaption>System Application added as dependency</figcaption></figure><p>Finally, we can witness a great piece of documented source code if we go to the Temp Blob codeunit definition.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-21.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="1187" height="794" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-21.png 600w, https://ashirokikh.com/content/images/size/w1000/2021/11/image-21.png 1000w, https://ashirokikh.com/content/images/2021/11/image-21.png 1187w" sizes="(min-width: 720px) 720px"><figcaption>Temp Blob codeunit</figcaption></figure><h2 id="conclusion">Conclusion</h2><p>Following the steps in this article, we have set up an environment where we can navigate across our extensions along with Base and System Applications. Our application can even be published from the workspace - just select the HelloWorld.al file on the list and hit F5. </p><p>The workspace can be a bit slow to work though. The good news is that this setup also works if we decide to open only our application folder for faster development when symbols are just enough. We can always return back to the VS Code workspace if we need better navigation without using any other tool. </p><p>If anything didn&apos;t work for you, or something could be done better, please let me know in the comments. Happy sailing!</p><hr><p>There are a couple of tips that can help with the setup in specific cases.</p><h3 id="tip-1">Tip #1</h3><p>Disabling VS Code extensions for the workspace can improve processing performance. The AL Language extension is a must-have, of course.</p><h3 id="tip-2">Tip #2</h3><p>If you&apos;re developing against a localized Business Central version, ensure to use the source code for the country. </p><h3 id="tip-3">Tip #3</h3><p>Ensure the source code in the referenced folders belongs to the same version as the symbols, or remove the app symbols with code in a workspace. For instance, if we put the <code>Microsoft_System Application_19.1.31886.32889.app</code> file into the <code>alpackages</code> folder, and System Application source code that we have in a workspace folder is of version <code>19.1.31886.32186</code>, we might get confusing at the first sight errors. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image-10.png" class="kg-image" alt="Navigating Microsoft Base and System Apps with VS Code Workspaces" loading="lazy" width="1044" height="143" srcset="https://ashirokikh.com/content/images/size/w600/2021/11/image-10.png 600w, https://ashirokikh.com/content/images/size/w1000/2021/11/image-10.png 1000w, https://ashirokikh.com/content/images/2021/11/image-10.png 1044w" sizes="(min-width: 720px) 720px"><figcaption>Ambiguous .NET reference error</figcaption></figure><p>Adding a dependency in <code>app.json</code> to an existing app in the workspace removes the requirement to have symbols.</p>]]></content:encoded></item><item><title><![CDATA[Convert Business Central BLOB Value to Text in Transact-SQL with SQL CLR]]></title><description><![CDATA[This blog post is for you if you're looking for a way to get the text values of a BLOB field directly from a SQL query without having to modify the Business Central code base.]]></description><link>https://ashirokikh.com/converting-business-central-blob-to-text-with-sql-clr/</link><guid isPermaLink="false">61907ade888fb23fe82825ee</guid><category><![CDATA[SQL Server]]></category><category><![CDATA[Walkthrough]]></category><dc:creator><![CDATA[Alex Shirokikh]]></dc:creator><pubDate>Sun, 14 Nov 2021 22:00:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1533750204176-3b0d38e9ac1e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDZ8fGRhdGFiYXNlfGVufDB8fHx8MTYzNjg1ODk2NQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1533750204176-3b0d38e9ac1e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDZ8fGRhdGFiYXNlfGVufDB8fHx8MTYzNjg1ODk2NQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Convert Business Central BLOB Value to Text in Transact-SQL with SQL CLR"><p>Building a custom reporting system with direct SQL queries to the Business Central can be done with a good understanding of the application data types and how values are persisted in the database. One interesting issue I&apos;ve come across recently is reading a text value from a BLOB field. This blog post is for you if you&apos;re looking for a way to get the text values of a BLOB field directly from a SQL query without having to modify the Business Central code base.</p><h3 id="the-problem">The Problem</h3><p>A BLOB field in Business Central corresponds to a database column of type image. It can hold text values far longer than a table field of type Text. Therefore, BLOB has been a reliable choice of a column type when we need to provide a way for adding detailed comments or descriptions where the length limitation of 250 characters would be an issue. </p><p>If a BLOB field is for storing a text value, it usually has its handler methods in the table which might look as follows (example from the <code>Sales Header</code> table, field <code>Work Description</code>):</p><figure class="kg-card kg-code-card"><pre><code class="language-al">    procedure GetWorkDescription() WorkDescription: Text
    var
        TypeHelper: Codeunit &quot;Type Helper&quot;;
        InStream: InStream;
    begin
        CalcFields(&quot;Work Description&quot;);
        &quot;Work Description&quot;.CreateInStream(InStream, TEXTENCODING::UTF8);
        if not TypeHelper.TryReadAsTextWithSeparator(InStream, TypeHelper.LFSeparator(), WorkDescription) then
            Message(ReadingDataSkippedMsg, FieldCaption(&quot;Work Description&quot;));
    end;
    
    procedure SetWorkDescription(NewWorkDescription: Text)
    var
        OutStream: OutStream;
    begin
        Clear(&quot;Work Description&quot;);
        &quot;Work Description&quot;.CreateOutStream(OutStream, TEXTENCODING::UTF8);
        OutStream.WriteText(NewWorkDescription);
        Modify;
    end;
</code></pre><figcaption>Getting and setting BLOB value in AL</figcaption></figure><p>The text value is saved by creating a stream for the BLOB field and writing the text into it. As a result, it&apos;s stored as raw bytes in the SQL server database. It wouldn&apos;t be a big problem to convert the saved value from bytes to text with Transact-SQL. The SQL query would be as follows:</p><figure class="kg-card kg-code-card"><pre><code class="language-sql">SELECT CONVERT(varchar(max), CONVERT(varbinary(max), [Work Description])) as [Work Description]
FROM [CRONUS Australia Pty_ Ltd_$Sales Header] with(nolock)
WHERE [Work Description] IS NOT NULL
</code></pre><figcaption>SQL query to convert an image column value to text</figcaption></figure><p>There is one little detail, however. The bytes are compressed by Business Central before they are sent to the SQL server. The saved <code>Work description</code> text value into the field would be returned by the SQL query as <code>E}[&#xCF;/&#xCA;VHI-N.&#xCA;,(&#xC9;&#xCC;&#xCF;</code>, which we might doubt is useful, obviously.</p><h3 id="an-option">An Option</h3><p>The property that is responsible for BLOB value compression is <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-compressed-property?ref=ashirokikh.com">Compressed</a>, its default value is <code>true</code>.</p><p>&quot;Aha! We just need to change that property to <code>false</code> and it&apos;s solved! And here are some <a href="https://stackoverflow.com/a/6922634?ref=ashirokikh.com">Stack Overflow discussions</a> that prove that it&apos;s working.&quot;, we might get excited. But then we remind ourselves that every non-default behavior implemented in source code requires maintenance in the future, and we should find the right solution for ourselves. </p><p>Maybe the reporting team is not responsible for the code and it&apos;s a dependency on other teams. Or maybe it&apos;s a third-party product that we have no control over. We should keep away from customizing the third-party or standard base application code, and there is no option to extend a BLOB table field by overriding the <code>Compressed</code> property value in an AL app. Even if we make the field change happen now, how about all the rest compressed by default BLOB fields that we have to alter the source code for? The way to make it work on the SQL side without having to change anything in the application could be a good solution in these cases.</p><h3 id="the-sql-clr-solution">The SQL CLR Solution</h3><p>The task is simple. We need to decompress the byte data before converting it to text. &#xA0;The built-in <code><a href="https://docs.microsoft.com/en-us/sql/t-sql/functions/decompress-transact-sql?ref=ashirokikh.com">DECOMPRESS</a></code> T-SQL function can look to be useful, but it&apos;s using the GZIP algorithm and not working for us.</p><p>There are multiple articles showing successful implementation of reading text from image fields. For example, <a href="https://devch.wordpress.com/2014/01/21/accessing-compressed-blobs-from-outside-nav-nav2013-revisited/?ref=ashirokikh.com">Accessing Compressed Blobs from outside NAV (NAV2013) (Revisited) | deV.ch - man vs. code (wordpress.com)</a>. The conclusion that we can make from that C# code is that we&apos;re missing 2 pieces in T-SQL: there are magic bytes in compressed BLOB values that we have to check, and the values are compressed with the Deflate algorithm. Once these two problems are addressed in SQL Server, we should be able to query the database directly. </p><p>Now, how can we use .NET code to extend SQL Server functionality? With <a href="https://docs.microsoft.com/en-us/sql/relational-databases/clr-integration/common-language-runtime-integration-overview?ref=ashirokikh.com">SQL Server CLR integration</a>, a small class can be written for value handling which can be compiled as a library. The library can be uploaded to the server as an assembly, which can serve as a foundation for our custom T-SQL function. </p><p>Following the <a href="https://docs.microsoft.com/en-us/sql/relational-databases/clr-integration/database-objects/getting-started-with-clr-integration?ref=ashirokikh.com">Getting Started with CLR Integration</a> article, I needed a .NET Framework 4.8 installed that is compatible with the SQL Server 2019 on my computer (the framework version should be compatible with SQL Server versions 2012+ according to <a href="https://stackoverflow.com/questions/41114147/which-version-of-net-framework-sql-server-supports?ref=ashirokikh.com">this source</a>). Here is the code of the class to compile:</p><figure class="kg-card kg-code-card"><pre><code class="language-csharp">using System.Linq;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.IO;
using System.IO.Compression;

public class BcBlobDecompressor
{
    private static readonly byte[] BlobMagic = new byte[] { 2, 69, 125, 91 };

    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true)]
    public static SqlBytes Decompress(SqlBytes inBytes)
    {
        if (inBytes == null || inBytes.Length &lt; 4)
            return inBytes;

        byte[] firstFourBytes = inBytes.Buffer.Take(4).ToArray();
        if (!BlobMagicOk(firstFourBytes))
        {
            return inBytes;
        }

        using (Stream inBytesStream = inBytes.Stream)
        {
            inBytesStream.Read(firstFourBytes, 0, 4);
            var outStream = new MemoryStream();
            using (DeflateStream deflateStream = new DeflateStream(inBytesStream, CompressionMode.Decompress))
            {
                deflateStream.CopyTo(outStream);
                return new SqlBytes(outStream);
            }
        }                
    }
    private static bool BlobMagicOk(byte[] checkMagic)
    {
        return checkMagic.SequenceEqual(BlobMagic);
    }
}
</code></pre><figcaption>BcBlobDecompressor class</figcaption></figure><p>The code can be copy-pasted into a file <code>BcBlobDecompression.cs</code> which can be put in, let&apos;s say, <code>C:\temp\sqlclr\</code> folder. If we run <code>cd C:\temp\sqlclr</code> in the terminal to make it the current folder, the PowerShell compilation command then would look like as follows:</p><figure class="kg-card kg-code-card"><pre><code class="language-powershell">C:\temp\sqlclr\&gt; C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library .\BcBlobDecompression.cs
</code></pre><figcaption>PowerShell command to compile BcBlobDecompressor class</figcaption></figure><p>The result of the steps is the <code>BcBlobDecompression.dll</code> file that will be used for our BC BLOB decompression function.</p><p>The next step is uploading the assembly to the SQL Server. Firstly, let&apos;s enable CLR on the server by following the <a href="https://docs.microsoft.com/en-us/sql/relational-databases/clr-integration/clr-integration-enabling?ref=ashirokikh.com">Microsoft docs article</a>.</p><figure class="kg-card kg-code-card"><pre><code class="language-sql">EXEC sp_configure &apos;clr enabled&apos;, 1;  
RECONFIGURE;  
GO  
</code></pre><figcaption>SQL script to enable CLR</figcaption></figure><p>The SQL Server 2019 has <code>clr strict security</code> enabled by default and the <code>CREATE ASSEMBLY</code> request stated in the guide failed for me with the error: <code>CREATE or ALTER ASSEMBLY for assembly &apos;BcBlobDecompression&apos; with the SAFE or EXTERNAL_ACCESS option failed because the &apos;clr strict security&apos; option of sp_configure is set to 1. Microsoft recommends that you sign the assembly with a certificate or asymmetric key that has a corresponding login with UNSAFE ASSEMBLY permission. Alternatively, you can trust the assembly using sp_add_trusted_assembly.</code> </p><p>If we use our decompression library on a production server, we&apos;d better not turn off security features for the sake of it. Having looked at the best practices following <a href="https://sqlquantumleap.com/2017/08/16/sqlclr-vs-sql-server-2017-part-3-clr-strict-security-solution-2/?ref=ashirokikh.com">this blog post</a>, the following assembly installation script was created.</p><figure class="kg-card kg-code-card"><pre><code class="language-sql">-- Source: https://pastebin.com/mwi5BidL

use [master]
-- Verify that &quot;clr strict security&quot; is 1 (i.e. ON), and that TRUSTWORTHY is 0 (i.e. OFF):
PRINT CHAR(13) + CHAR(10) + &apos;Verifying settings...&apos;;
SELECT *
FROM   sys.configurations sc
WHERE  sc.[configuration_id] = 1587 -- &quot;clr strict security&quot;
 
SELECT [name], [is_trustworthy_on], [collation_name]
FROM sys.databases WHERE [database_id] = DB_ID(N&apos;YourDatabaseName&apos;);
GO

USE [YourDatabaseName]

-----------------------------------------------------------
-- Create asembly and function:

IF (OBJECT_ID(N&apos;dbo.DecompressBcBlob&apos;) IS NULL)
BEGIN

	PRINT CHAR(13) + CHAR(10) + &apos;SQLCLR UDF [DecompressBcBlob] does not exist:&apos;;

	PRINT &apos; TEMPORARILY Altering Database to set TRUSTWORTHY=ON...&apos;;
		ALTER DATABASE [YourDatabaseName]
			SET TRUSTWORTHY ON;

	IF (ASSEMBLYPROPERTY(N&apos;BcBlobDecompression&apos;, N&apos;MvID&apos;) IS NULL)
	BEGIN
		PRINT &apos; Creating [BcBlobDecompression] Assembly...&apos;;

		CREATE ASSEMBLY BcBlobDecompression 
		AUTHORIZATION  dbo
		FROM &apos;C:\temp\sqlclr\BcBlobDecompression.dll&apos; WITH PERMISSION_SET = SAFE;
	END;

	PRINT &apos; Altering Database to set TRUSTWORTHY back to OFF...&apos;;
	ALTER DATABASE [YourDatabaseName]
		SET TRUSTWORTHY OFF;

	PRINT N&apos;    Creating [dbo].[DecompressBcBlob]...&apos;;
		EXEC(N&apos;
	CREATE FUNCTION [dbo].[DecompressBcBlob]
	(
		@BlobValue VARBINARY(MAX)
	)
	RETURNS VARBINARY(MAX)
	WITH    EXECUTE AS CALLER,
			RETURNS NULL ON NULL INPUT
	AS EXTERNAL NAME [BcBlobDecompression].[BcBlobDecompressor].[Decompress];
	&apos;);

END;
GO

-----------------------------------------------------------
-- Sign assembly for the database:
 
IF (SUSER_ID(N&apos;YourDatabaseName-BcBlobDecompression-Login&apos;) IS NULL)
BEGIN
    PRINT CHAR(13) + CHAR(10) + &apos;Permission Login does NOT exist:&apos;;
 
    IF (CERT_ID(N&apos;YourDatabaseName-BcBlobDecompression-Cert&apos;) IS NULL)
    BEGIN
        PRINT &apos; Creating Certificate in [YourDatabaseName]...&apos;;
        CREATE CERTIFICATE [YourDatabaseName-BcBlobDecompression-Cert]
            ENCRYPTION BY PASSWORD = &apos;CiKqB9wv88wYCB&apos;
            WITH SUBJECT = &apos;Sql Quantum Leap&apos;,
            EXPIRY_DATE = &apos;2099-12-31&apos;;
    END;
 
    IF (NOT EXISTS(
                    SELECT *
                    FROM  sys.crypt_properties cp
                    INNER JOIN sys.assemblies sa
                            ON sa.[assembly_id] = cp.[major_id]
                    WHERE sa.[name] = N&apos;BcBlobDecompression&apos;
                ))
    BEGIN
        PRINT &apos; Signing the Assembly...&apos;;
        ADD SIGNATURE
            TO Assembly::[BcBlobDecompression]
            BY CERTIFICATE [YourDatabaseName-BcBlobDecompression-Cert]
            WITH PASSWORD = &apos;CiKqB9wv88wYCB&apos;;
 
    END;        
 
    IF (NOT EXISTS(
                    SELECT *
                    FROM   [master].[sys].[certificates] crt
                    WHERE  crt.[name] = N&apos;YourDatabaseName-BcBlobDecompression-Cert&apos;
                ))
    BEGIN
        PRINT &apos; Copying the Certificate to [master]...&apos;;
        DECLARE @PublicKey VARBINARY(MAX),
                @SQL NVARCHAR(MAX);
 
        SET @PublicKey = CERTENCODED(CERT_ID(N&apos;YourDatabaseName-BcBlobDecompression-Cert&apos;));
 
        SET @SQL = N&apos;
CREATE CERTIFICATE [YourDatabaseName-BcBlobDecompression-Cert]
    FROM BINARY = &apos; + CONVERT(NVARCHAR(MAX), @PublicKey, 1) + N&apos;;&apos;;
        --SELECT @PublicKey AS [@PublicKey]; -- DEBUG
        --PRINT @SQL; -- DEBUG
 
        EXEC [master].[sys].[sp_executesql] @SQL;
    END;
 
 
    PRINT &apos; Creating permissions Login...&apos;;
    EXEC [master].[sys].[sp_executesql] N&apos;
CREATE LOGIN [YourDatabaseName-BcBlobDecompression-Login]
    FROM CERTIFICATE [YourDatabaseName-BcBlobDecompression-Cert];
&apos;;
END;
GO

PRINT CHAR(13) + CHAR(10) + &apos;Granting UNSAFE ASSEMBLY permission...&apos;;
EXEC [master].[sys].[sp_executesql] N&apos;
GRANT UNSAFE ASSEMBLY TO [YourDatabaseName-BcBlobDecompression-Login]; -- REQUIRED!!!!
&apos;; 
GO

PRINT &apos;&apos;;
SELECT ASSEMBLYPROPERTY(N&apos;BcBlobDecompression&apos;, &apos;CLRName&apos;) AS [CLRName];

/*
-----------------------------------------------------------
-- Clean up:
 
PRINT CHAR(13) + CHAR(10) + &apos;Cleaning up objects (to be re-runnable)...&apos;;
 
USE [YourDatabaseName]
DROP FUNCTION [dbo].[DecompressBcBlob]
DROP ASSEMBLY [BcBlobDecompression];
DROP CERTIFICATE [YourDatabaseName-BcBlobDecompression-Cert];
 
USE [master];
DROP LOGIN [YourDatabaseName-BcBlobDecompression-Login];
DROP CERTIFICATE [YourDatabaseName-BcBlobDecompression-Cert];
*/
</code></pre><figcaption>SQL script to install the library</figcaption></figure><p>In order to run it, replace the <code>YourDatabaseName</code> string with your database name and the password for assembly signing can be changed as well. The script works per database, hence the database names utilization. The clean-up section needs to be run whenever a new version of the assembly needs to be created. I had to ensure the database owner is the <code>sa</code> user (or the same as the owner of the <code>master</code> database).</p><p>If the script runs without errors, you now should be able to check if the <code>DecompressBcBlob</code> function works by running the following examples:</p><figure class="kg-card kg-code-card"><pre><code class="language-sql">select dbo.DecompressBcBlob(null)
select dbo.DecompressBcBlob(0x02457D5B0B29CF0700)
select dbo.DecompressBcBlob(0x54776F)
</code></pre><figcaption>Test SQL script</figcaption></figure><p>The results should look as follows. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2021/11/image.png" class="kg-image" alt="Convert Business Central BLOB Value to Text in Transact-SQL with SQL CLR" loading="lazy" width="487" height="232"><figcaption>dbo.DecompressBcBlob results</figcaption></figure><p>The first select query checks if the <code>NULL</code> value is handled. The second one passes a compressed text example. The third result returns the same uncompressed value as if the <code>Compressed</code> property of the BLOB field is set to <code>false</code>.</p><p>The SQL query that would return text value from a Business Central BLOB field may look like as follows:</p><figure class="kg-card kg-code-card"><pre><code class="language-sql">SELECT CONVERT(varchar(max), dbo.DecompressBcBlob(CONVERT(varbinary(max), [Work Description]))) as [Work Description]
FROM [CRONUS Australia Pty_ Ltd_$Sales Header] with(nolock)
WHERE [Work Description] IS NOT NULL
</code></pre><figcaption>SQL query to convert an image column value to text with DecompressBcBlob function</figcaption></figure><p>It&apos;s working but slightly unreadable. How about creating another function for this? To do that, run the following SQL script.</p><figure class="kg-card kg-code-card"><pre><code class="language-sql">CREATE FUNCTION dbo.ConvertBcBlobToText
(
	@i image
)
RETURNS varchar(max)
AS
BEGIN
	RETURN CONVERT(varchar(max), dbo.DecompressBcBlob(CONVERT(varbinary(max), @i)));
END
GO
</code></pre><figcaption>SQL script to create ConvertBcBlobToText function</figcaption></figure><p>With that last enhancement, our query becomes as elegant as follows:</p><figure class="kg-card kg-code-card"><pre><code class="language-sql">SELECT dbo.ConvertBcBlobToText([Work Description]) as [Work Description]
FROM [CRONUS Australia Pty_ Ltd_$Sales Header] with(nolock)
WHERE [Work Description] IS NOT NULL
</code></pre><figcaption>SQL query to convert an image column value to text with ConvertBcBlobToText function</figcaption></figure><h3 id="conclusion">Conclusion</h3><p>Using this SQL CLR solution for Business Central BLOB field value decompression can be a good fit for a system built on a SQL Server on top of the application database using Transact-SQL. The assembly adds just the necessary function that the server needs to understand a compressed value by Business Central. It would allow you to build your own logic for reading the bytes and converting them into text. </p>]]></content:encoded></item><item><title><![CDATA[Measuring Code Performance with NAV Application Profiler]]></title><description><![CDATA[This article brings to attention that the good old tool is still out there and can be incredibly helpful for on-premise Business Central (v 14).]]></description><link>https://ashirokikh.com/measuring-code-performance-with-nav-application-profiler/</link><guid isPermaLink="false">5f8ae4ef88eb286a4bb5858d</guid><category><![CDATA[Performance]]></category><category><![CDATA[Business Central]]></category><category><![CDATA[NAV]]></category><dc:creator><![CDATA[Alex Shirokikh]]></dc:creator><pubDate>Mon, 19 Oct 2020 10:10:12 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1508344466289-d19bdcb42776?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1508344466289-d19bdcb42776?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Measuring Code Performance with NAV Application Profiler"><p>We expect our Business Central or NAV application to perform well to accommodate the highest predicted volume of operations. Finding the root cause of a performance issue is often a challenging task especially if there is no good instrument for that on hand.</p><p>If you are running Business Central in the cloud or have converted your solution to AL (including on-premise versions), it is recommended to look into monitoring extensions with Application Insights (<a href="https://simplanova.com/blog/monitoring-dynamics-365-business-central-al-functions-performance-app-insights/?ref=ashirokikh.com">an example from Dmitry Katson</a>) which is the industry standard for Microsoft cloud solutions. Otherwise, in case your system is still running on premise where we can use custom .NET libraries, the performance profiler I&#x2019;m going to talk about in this article is definitely worth a shot. </p><h2 id="nav-application-profiler-is-still-out-there">NAV Application Profiler Is Still Out There</h2><p>The <a href="https://github.com/wortho/EtwPerformanceProfiler?ref=ashirokikh.com">NAV Application Profiler (EtwPerformanceProfiler) repository</a> description states that it is a sample for profiling application code created by <a href="https://www.linkedin.com/in/davidworthington/?ref=ashirokikh.com">David Worthington</a> and <a href="https://www.linkedin.com/in/dmytrositnik/?ref=ashirokikh.com">Dmytro Sitnik</a> (while being part of the Dynamics NAV team) in 2013 and <a href="https://cloudblogs.microsoft.com/dynamics365/no-audience/2014/07/04/introducing-the-microsoft-dynamics-nav-application-profiler/?ref=ashirokikh.com">announced in 2014</a>. ETW stands for Event Tracing for Windows, and the application is based on the <em>Microsoft.Diagnostics.Tracing.TraceEvent</em> library which, if interested, you could get familiar with by reading <a href="https://github.com/microsoft/perfview/blob/master/documentation/TraceEvent/TraceEventLibrary.md?ref=ashirokikh.com">the library documentation</a>. One interesting fact I learnt from it is that tracing with the library is <a href="https://github.com/microsoft/perfview/blob/master/documentation/TraceEvent/TraceEventProgrammersGuide.md?ref=ashirokikh.com#asynchronous">asynchronous </a>and therefore has almost no performance impact and it should be safe to run it on a production instance.</p><p>About a year ago (mid 2019), shortly after we upgraded NAV 2009 to Business Central version 14 and brought all our data, the load in production indicated some symptoms of code inefficiency by showing messages like a &#x201C;table was locked by another user&#x201D;. We needed something that would point us to the bottleneck and do it fast.</p><p>Having had a chance to get familiar with NAV Application Profiler before, I urged to try it but quickly realized it hasn&#x2019;t been updated for some time and the last version it was updated to was NAV 2018 (according to the commit history). I used that opportunity to refresh my C# skills and after some time the profiler started working for our version of BC. Since then, our team has adopted the tool and now runs it every time before deploying a change to the codebase.</p><p>The fix should widen the range of supported platforms, the result of which is in the source code of the GitHub repository.</p><h2 id="installation-steps">Installation Steps</h2><p>The NAV Application Profiler repository is public, therefore anyone can clone and play with it. The .NET libraries can be built by Visual Studio 2019 Community edition which can be used for free for open source projects.</p><p>In case you would like to just download the installation package, I&apos;ve build it for you. Please use the following link where I&apos;ve put them together:</p><p><a href="https://ashirokikh.com/downloads/navappprofiler-2020-10-18.zip">Download NAV Application Profiler files (1.5MB, built on 18/10/2020)</a></p><p>After you have the files, the next actions need to be performed:</p><!--kg-card-begin: markdown--><ol>
<li>Extract the files.</li>
<li>Copy the EtwPerformanceProfiler folder to the following locations
<ol>
<li>Server Add-ins folder of BC. By default, it&#x2019;s <code>C:\Program Files\Microsoft Dynamics NAV\140\Service\Add-ins\</code> (for NAV 2013 it is <code>C:\Program Files\Microsoft Dynamics NAV\80\Service\Add-ins</code>)</li>
<li>Role Tailored Client Add-ins folder which should be <code>C:\Program Files (x86)\Microsoft Dynamics 365 Business Central\140\RoleTailored Client\Add-ins</code> (<code>C:\Program Files (x86)\Microsoft Dynamics NAV\80\RoleTailored Client\Add-ins</code> for NAV 2013)</li>
</ol>
</li>
<li>Ensure the Enable Full C/AL Function Tracing is turned on in the instance configuration and restart the instance if it wasn&#x2019;t.</li>
<li>Import and compile the objects from the App Objects folder. The BC instance should see the libraries already. However, the libraries might get blocked by the system, which we can resolve by running an <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/unblock-file?ref=ashirokikh.com">Unblock-File</a> PowerShell command or by manually unblocking them in the file properties.</li>
</ol>
<!--kg-card-end: markdown--><p>Now that we have our objects compiled, we can proceed to the most exciting part, seeing what are the most time-consuming lines in our code!</p><h2 id="running-nav-application-profiler">Running NAV Application Profiler</h2><p>The main application profiler page is the page 50000 Performance Profiler, let&apos;s run it from the Development Environment. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/10/image-5.png" class="kg-image" alt="Measuring Code Performance with NAV Application Profiler" loading="lazy" width="2000" height="949" srcset="https://ashirokikh.com/content/images/size/w600/2020/10/image-5.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/10/image-5.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/10/image-5.png 1600w, https://ashirokikh.com/content/images/size/w2400/2020/10/image-5.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Performance Profiler</figcaption></figure><p>It is possible to trace multiple sessions but we would often target our session to analyze performance. One of the ways of finding out the Session ID is using the Debug Session function and see all running sessions on the instance. I&apos;ve started another client session to run a user operation example and have now two sessions running, which you can see in the picture below. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/10/image-6.png" class="kg-image" alt="Measuring Code Performance with NAV Application Profiler" loading="lazy" width="2000" height="718" srcset="https://ashirokikh.com/content/images/size/w600/2020/10/image-6.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/10/image-6.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/10/image-6.png 1600w, https://ashirokikh.com/content/images/2020/10/image-6.png 2280w" sizes="(min-width: 720px) 720px"><figcaption>Session List</figcaption></figure><p>In the example below, I specified my second session as the target session and hit the Start action button to get the ball rolling.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/10/image-4.png" class="kg-image" alt="Measuring Code Performance with NAV Application Profiler" loading="lazy" width="2000" height="861" srcset="https://ashirokikh.com/content/images/size/w600/2020/10/image-4.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/10/image-4.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/10/image-4.png 1600w, https://ashirokikh.com/content/images/size/w2400/2020/10/image-4.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Performance Profiler Started</figcaption></figure><p>Now, using the second client, we could run the code that concerns us. I&apos;m going to open the Vendors page and then stop the profiler.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/10/image-7.png" class="kg-image" alt="Measuring Code Performance with NAV Application Profiler" loading="lazy" width="2000" height="1247" srcset="https://ashirokikh.com/content/images/size/w600/2020/10/image-7.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/10/image-7.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/10/image-7.png 1600w, https://ashirokikh.com/content/images/size/w2400/2020/10/image-7.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Performance Profiler Collected Tracing Results</figcaption></figure><p>After tracing data is collected, it&apos;s read by NAV Application Profiler and aggregated into the table with a hierarchical view. We can see the Object Types, Object IDs, C/AL, and SQL statements with duration and hit counts, all ready for our analysis. How amazing is that? If you are looking for a particular statement, you could try filtering the Statement column specifying <code>*&lt;my statement substring&gt;*</code> as the filter value and see how well that code line behaves itself.</p><p>The Copy To Archive function lets us save the results in the database to be able to refer to it later. The Archive action button can be used to open archived traced data. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/10/image-8.png" class="kg-image" alt="Measuring Code Performance with NAV Application Profiler" loading="lazy" width="2000" height="762" srcset="https://ashirokikh.com/content/images/size/w600/2020/10/image-8.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/10/image-8.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/10/image-8.png 1600w, https://ashirokikh.com/content/images/size/w2400/2020/10/image-8.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Performance Profiler Archive</figcaption></figure><h2 id="summary">Summary</h2><p>Deploying NAV Application Profiler to an on-premise Business Central or NAV can be a life-changer. I have caught myself multiple times already wrongly assuming a particular area was the culprit of the performance degradation. This tool can provide us with facts to address problems quickly and directly. </p><p>Business Central 2019 wave 1 (version 14) is going to stay with us for some time considering the next versions support AL applications only. Its mainstream support end date has already been moved to October 10, 2023, according to the <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/terms/lifecycle-policy-on-premises?ref=ashirokikh.com">Fixed Lifecycle Policy</a>. Even if all the customization logic has been moved to AL, there are some cloud limitations like the <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/faq?ref=ashirokikh.com#how-does-microsoft-handle-database-sizes">database size of 80 GB</a> or other reasons that would make you choose to stay an on-premises customer. The C/AL NAV Application Profiler objects can be converted to AL and you should still be able to use it happily after. &#xA0; </p><p>Although NAV Application Profiler can help troubleshoot performance issues significantly, it is best to avoid them at the design stage. Great places to look at to master the skill are the <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/performance/performance-developer?ref=ashirokikh.com">performance articles for developers</a> and numerous Dynamics 365 BC community blog posts (like <a href="https://robertostefanettinavblog.com/2020/02/09/business-central-how-to-make-it-go-fast/?ref=ashirokikh.com">this one by Roberto Stefanetti</a>). </p><p>I hope you enjoyed reading this post and the performance profiler helps to make your application or customization more efficient and your BC users happier. </p><hr><h2 id="additional-tips">Additional Tips</h2><h3 id="deploy-nav-application-profiler-to-a-container">Deploy NAV Application Profiler to a Container</h3><p>A BC container version 14 was used for the examples in this article. Below are the steps and scripts that were used.</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">$containerName = &apos;bc14dev&apos;
$imageName = &quot;mcr.microsoft.com/businesscentral/onprem:14.11.41204.0-au&quot;

$credential = New-Object pscredential &apos;dev&apos;, (ConvertTo-SecureString -String &apos;dev&apos; -AsPlainText -Force)

Measure-Command {
    New-BCContainer -accept_eula `
        -accept_outdated `
        -containerName $containerName `
        -imageName $imageName `
        -auth UserPassword `
        -credential $credential `
        -doNotExportObjectsToText `
        -memoryLimit 10G `
        -updateHosts `
        -includeCSide `
        -myscripts @()
}</code></pre><figcaption>BC container creation script</figcaption></figure><p>In order to import the text files and modify them, a license might be required. You should be able to import the fob file with all objects in any case.</p><p>Once the container is running, I ran the following</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">cd C:\Temp\NAVAppProfiler\ # your unzipped folder
Compress-Archive -Path .\EtwPerformanceProfiler\* -DestinationPath .\EtwPerformanceProfiler.zip
Copy-FileToBcContainer -containerName bc14dev -localPath .\EtwPerformanceProfiler.zip -containerPath &apos;c:\Temp\EtwPerformanceProfiler.zip&apos;
Open-BcContainer bc14dev
</code></pre><figcaption>Copying the libraries to the container and opening container PowerShell</figcaption></figure><p>The below commands were executed in the container PowerShell session.</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Expand-Archive -Path C:\Temp\EtwPerformanceProfiler.zip -DestinationPath C:\Temp\EtwPerformanceProfiler\
Copy-Item -Path &quot;C:\Temp\EtwPerformanceProfiler\&quot; -Destination &quot;C:\Program Files\Microsoft Dynamics NAV\140\Service\Add-ins\&quot; -Recurse
Set-NAVServerConfiguration -ServerInstance nav -KeyName EnableFullALFunctionTracing -KeyValue true
Get-NAVServerInstance | Restart-NAVServerInstance -Force -Verbose</code></pre><figcaption>Commands to unzip the archive and copy files to the Service Add-ins folder</figcaption></figure><p>BC Container Helper creates the RoleTailered Client folder on your machine, therefore the libraries need to be copied to <code>C:\ProgramData\BcContainerHelper\Extensions\bc14dev\Program Files\140\RoleTailored Client\Add-ins</code> to be able to compile the objects in NAV Development Environment. &#xA0;</p><p>The final step is importing the BC objects, compiling them with the Synchronize Schema option set to Later, and tenant synchronization with the following command in the container&apos;s PowerShell terminal.</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Get-NAVServerInstance | Sync-NAVTenant -Force -Verbose</code></pre><figcaption>Command to synchronize schema for all instances</figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Walkthrough: AL Extension on Docker Desktop]]></title><description><![CDATA[Follow this detailed guide to set up your first Business Central development environment with BC Container Helper and publish an AL extension to it.]]></description><link>https://ashirokikh.com/walkthrough-al-extension-on-docker-desktop/</link><guid isPermaLink="false">5ecbb4de72ff8e07ea9966ba</guid><category><![CDATA[Docker]]></category><category><![CDATA[BcContainerHelper]]></category><category><![CDATA[NavContainerHelper]]></category><category><![CDATA[Walkthrough]]></category><category><![CDATA[Business Central]]></category><dc:creator><![CDATA[Alex Shirokikh]]></dc:creator><pubDate>Fri, 25 Sep 2020 08:14:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1523351964962-1ee5847816c3?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1523351964962-1ee5847816c3?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Walkthrough: AL Extension on Docker Desktop"><p>Spending time efficiently while working is mainly about having proper tools. If you&apos;re a software engineer, there are many points <a href="https://www.docker.com/why-docker?ref=ashirokikh.com">why Docker</a> is worth investing your time. The main argument for me as a D365 Business Central (BC) developer is that you can keep your computer clean of BC installations and work with multiple versions of the system at the same time!</p><p>This post is for you if you still think that Business Central (BC) development using Docker is something difficult to start with. <a href="https://github.com/freddydk?ref=ashirokikh.com">Freddy Kristiansen</a> (Microsoft) with help of other contributors has done a great job to make it easier for everyone to include this amazing tool into our collection by introducing a <a href="https://freddysblog.com/2020/08/11/bccontainerhelper/?ref=ashirokikh.com">BC Container Helper</a>.</p><p>If you are teamed up with an end user and don&#x2019;t have the Microsoft Partner portal access, using Docker is a very liberating experience with regard to being able to play with the source code of multiple application versions. Have you ever wanted to quickly get an original BC object without customization, or to see how the next version looks like? Now it can be done with spinning up a local BC environment with the version.</p><p>Convinced to give it a try? Let&apos;s get to it!</p><h2 id="prerequisites">Prerequisites</h2><p>The prerequisites for this walk-through are as follows:</p><!--kg-card-begin: markdown--><ul>
<li>Windows 10 Enterprise, Pro, or Education (Hyper-V virtualisation is required for Docker on Windows).</li>
<li>Good unlimited Internet connection.</li>
</ul>
<!--kg-card-end: markdown--><p>The steps below were run on a Virtual Machine (VM) based from the <a href="https://developer.microsoft.com/en-us/windows/downloads/virtual-machines/?ref=ashirokikh.com">Windows 10 dev environment</a> image to ensure everything is done on a clean machine. See the VM specific extra steps below if you use a VM. </p><h2 id="getting-docker-desktop">Getting Docker Desktop</h2><p>When I try to search for Docker, a popular search engine points me at <a href="https://hub.docker.com/editions/community/docker-ce-desktop-windows?ref=ashirokikh.com">https://hub.docker.com/editions/community/docker-ce-desktop-windows</a> for the Docker Desktop installation resources. We need the Community Edition (CE) version.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-2.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Docker Desktop for Windows page</figcaption></figure><p>You don&#x2019;t have to have a Docker account to complete this step. Download the Stable version package, run it and follow the installation instructions. </p><p>Once the installation is completed, we need to switch to Windows containers using the Docker tray icon.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-6.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="934" height="852" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-6.png 600w, https://ashirokikh.com/content/images/2020/09/image-6.png 934w" sizes="(min-width: 720px) 720px"><figcaption>Switch to Windows containers menu item</figcaption></figure><p>If the Containers feature has not been enabled, Docker will exit for the error.</p><figure class="kg-card kg-image-card"><img src="https://ashirokikh.com/content/images/2020/09/image-5.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="1728" height="635" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-5.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-5.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/09/image-5.png 1600w, https://ashirokikh.com/content/images/2020/09/image-5.png 1728w" sizes="(min-width: 720px) 720px"></figure><p>That can be fixed by running the following PowerShell command as Administrator and restarting the computer.</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Enable-WindowsOptionalFeature -Online -FeatureName Containers -All</code></pre><figcaption>Command to enable the Containers feature</figcaption></figure><p>The version of Docker Desktop that was used for this walkthrough is 2.3.0.5.</p><h2 id="getting-bc-container-helper">Getting BC Container Helper</h2><p>Open a PowerShell window as Administrator and paste the following command that should install the version 1.0.4 (current at the moment of writing) of the module,</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Install-Module -Name BcContainerHelper -RequiredVersion 1.0.4</code></pre><figcaption>BcContainerHelper installation command</figcaption></figure><p>confirming installation of the dependencies along the way. The <a href="https://www.powershellgallery.com/packages/BcContainerHelper/1.0.4?ref=ashirokikh.com">PowerShell Gallery </a>lists the module versions and specifying the latest stable version is usually a good idea.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>BC Container Helper installation output</figcaption></figure><h2 id="creating-a-business-central-container">Creating a Business Central Container</h2><p>Ensure Docker Desktop is running and run Docker Desktop if it&apos;s not there.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh6.googleusercontent.com/DIAr8xBDsx7s-892x-Bn45Hf-wizYyZU-FAPlCcSvVIt7rbjXDcljz3b-kNGZv9juLfchi7tbuLEqSajGHMGw0Gae87iadVzdqrM2zMtIK4euGLubnRY2j6SKY9rrnNaNb9M-bMy" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Docker tray icon</figcaption></figure><p>Your environment might have the required execution policy. Otherwise, set it to <code>RemoteSigned</code> with the following command in case there are issues with running the scripts below.</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Set-ExecutionPolicy RemoteSigned</code></pre><figcaption>Setting execution policy to run scripts</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh6.googleusercontent.com/X1sNzW0IWCoaAGkxDrSWI4ItPS-B6ZY483ZSLrNcHcEQCw9PjV0qoIVyc58F6X5CdHELnuF_nbv2KA5kH5yXr8yXqjke7PCfXMYsMgyriDPN37N--hjC2JlPlKICNiOArWHjcFGw" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Setting execution policy output</figcaption></figure><p>Check the module is working by typing in the following.</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Write-BcContainerHelperWelcomeText</code></pre><figcaption>Write-BcContainerHelperWelcomeText command</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-7.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="1965" height="1385" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-7.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-7.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/09/image-7.png 1600w, https://ashirokikh.com/content/images/2020/09/image-7.png 1965w" sizes="(min-width: 720px) 720px"><figcaption>Write-BcContainerHelperWelcomeText output</figcaption></figure><p>To ensure the module has the permissions required, run the command below. </p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Check-BcContainerHelperPermissions -Fix</code></pre><figcaption>Command for fixing permissions for the module</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-8.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="1965" height="571" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-8.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-8.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/09/image-8.png 1600w, https://ashirokikh.com/content/images/2020/09/image-8.png 1965w" sizes="(min-width: 720px) 720px"><figcaption>Check-BcContainerHelperPermissions -Fix output</figcaption></figure><p>I&#x2019;m using the Australian version so the image tag is <em>au-ltsc2019. </em>Run the following script to create the latest available on Docker Business Central (update the image tag if required). You could save it in a file with the <em>.ps1</em> extension and put somewhere close. The script should not require running as Administrator. </p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">$artifactUrl = Get-BCArtifactUrl -version 16 -country au -select Latest
$containerName = &quot;bclts&quot;
$authenticationType = &apos;NavUserPassword&apos; # or Windows for the current AD user

Write-Host &quot;*** Starting $containerName container creation ***&quot; -ForegroundColor Yellow
Write-Host &quot;Artifact URL: $artifactUrl&quot;

if ($authenticationType -ne &apos;Windows&apos;){
    $credential = get-credential -Message &quot;Enter BC Super User credentials&quot;
}
Measure-command {
    New-BCContainer -accept_eula `
                    -accept_outdated `
                    -containerName $containerName `
                    -artifactUrl $artifactUrl `
                    -auth $authenticationType `
                    -credential $credential `
                    -memoryLimit 10G `
                    -updateHosts `
                    -includeAL `
                    -myscripts @()
}</code></pre><figcaption>BC Container creation PowerShell script</figcaption></figure><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh6.googleusercontent.com/HTzNBg6-yXBOcOJ6g7uaXp5kDWhNSLiIMmmKSqCmtluXMFNLsmZ5yow9BVEakwxlcujORWF90_HyYNxdE-bsG-3Bqn3xqhDU_BYSNP13MP1i5B3ZwWHGcfkvcwmEEG5ldU02KLv8" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Running a saved PowerShell script</figcaption></figure><p>Enter the container administrator user credentials if you&#x2019;re asked to.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-12.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="802" height="651" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-12.png 600w, https://ashirokikh.com/content/images/2020/09/image-12.png 802w" sizes="(min-width: 720px) 720px"><figcaption>Credentials dialog</figcaption></figure><p>Docker will start downloading the image. </p><figure class="kg-card kg-image-card"><img src="https://ashirokikh.com/content/images/2020/09/image-9.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="1965" height="580" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-9.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-9.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/09/image-9.png 1600w, https://ashirokikh.com/content/images/2020/09/image-9.png 1965w" sizes="(min-width: 720px) 720px"></figure><p>Running the script fist time will take quite a bit of time as it downloads docker images and artifacts (took ~30 minutes for me). Once it finishes, the result console look will resemble the below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-10.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="1892" height="1309" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-10.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-10.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/09/image-10.png 1600w, https://ashirokikh.com/content/images/2020/09/image-10.png 1892w" sizes="(min-width: 720px) 720px"><figcaption>BC Docker container creation output</figcaption></figure><p>Last output lines include some useful information about the container, such as the .vsix file location that we&#x2019;ll need to set up Visual Studio Code. In case the output info somehow got lost, the following command should give you enough data.</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">docker logs bclts</code></pre><figcaption>Command to get container debug information</figcaption></figure><p>You&#x2019;ll notice some newly created shortcuts on your Desktop.</p><p>Feel free to follow the Web Client link and confirm the application is working! If you follow the naming in this post, it should be <a href="http://bclts/BC/?ref=ashirokikh.com">http://bclts/BC/</a>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-14.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="1580" height="1296" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-14.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-14.png 1000w, https://ashirokikh.com/content/images/2020/09/image-14.png 1580w" sizes="(min-width: 720px) 720px"><figcaption>Web Client credentials dialog</figcaption></figure><p>After entering the credentials, you should see something like this.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-13.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="2000" height="1697" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-13.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-13.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/09/image-13.png 1600w, https://ashirokikh.com/content/images/2020/09/image-13.png 2164w" sizes="(min-width: 720px) 720px"><figcaption>Business Central Web Client running on Docker</figcaption></figure><p>Congratulations, you&#x2019;ve just created a working environment of the latest version of Business Central!</p><p>The beauty of using Docker is that no matter how creative you mess with the container, you can save your progress, delete the container, and spin up a new one in about 10 minutes! Once the images are downloaded to your machine, they&#x2019;ll be used for your next container as long as you use the same BC Docker image tag.</p><h2 id="setting-up-visual-studio-code">Setting Up Visual Studio Code</h2><p><a href="https://code.visualstudio.com/?ref=ashirokikh.com">Visual Studio Code</a> is the most popular IDE for AL development and is required to go further.</p><p>Download the vsix file using the address from your logs which contains the AL language version that matches the container. It&#x2019;s <a href="http://bclts:8080/ALLanguage.vsix?ref=ashirokikh.com">http://bclts:8080/ALLanguage.vsix</a> in my case.</p><p>When switching between the containers from different release waves, install the extension from the VSIX file that comes with the container if there are issues.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh5.googleusercontent.com/vG7d3uQOa8Y_5JW392v36FbemInZFXSiX2mt6f7T_IkR3pcvJMq7ucA0spMf2AW5efKedVIi2I3eQmGLmT2zF5Nm2agfYZotp3N37-0r--B8jw5dRlNDNYAFCzYuC9GVVZcACBmy" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>AL Language extension downloaded</figcaption></figure><p>Then open VS Code, go to the Extensions tab and hit the Install from VSIX menu item, and select the downloaded file.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh5.googleusercontent.com/Z413aK7dBUmfCsfVpuLwxLG4a0p3iHog0AVcfXVGxM0VMmgwrQGNEr8ROWQU5DE5wrfAzMQx7aQuqh0N8h7PwFVyl6a2W4wFp55fmqrxKsSHxveFVFmTpwD3E8gyy1zCz_-RosL1" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Visual Studio Code Install from VSIX menu item</figcaption></figure><p>After the extension is installed, hit <strong>Alt+A, Alt+L</strong> to trigger the <strong>AL Go!</strong> command, specify a to be created project folder and which version to run.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh5.googleusercontent.com/cmNhtcMTRCBDxVYSBT5VHjsGHSCiBoLGyvgvIvwQgXYPyuKoKz3vC6xCSzwm6WSAsPG-uwtbBBeqN344kQPubXO2eRUnFATc8vX2e3OZmbDVYjdVx1ihPsoBaX9CetyfPA_OCn63" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>AL extension folder path</figcaption></figure><p>Then select the latest platform.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh6.googleusercontent.com/2oypyL_1hsa0tPUTkmVMvO4QEve45BSXWrGhQcU5Tlo2ITVnrqgYRJ5wNFiZmpyJDBNEsGLGK4adbCqK4ufhJkyUaMsqO60ppVTU3fUl66vVGPmhOrLvSRYEUhJVgYR1j47B91hq" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Available Business Central platforms in VS Code</figcaption></figure><p>After the project forlder opens up automatically, select <em>Your own server</em> as the server when the dialog pops up.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh4.googleusercontent.com/7M3a31E0udl4WLSK5xiktRxl2WBAHQo1pec_zZNPEc1OJzKDCGffwiEck__zC-BDZi3w3N8TGttq5zxfXY-xpdoXM9E_UPIg-8WZTDfwm4eDgJ23HignBd12CjihU1EEkaJHjbl1" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Server AL extension setup dialog</figcaption></figure><p>Next, escape from the server authentication prompt as we need to update the <em>launch.json</em> to point to our container instance.</p><p>Now we need the container name that we specified before, and the <em>Dev. Server Instance</em> name that was mentioned in the logs. My result <em>launch.json</em> file looks as below.</p><figure class="kg-card kg-code-card"><pre><code class="language-JSON">{
    &quot;version&quot;: &quot;0.2.0&quot;,
    &quot;configurations&quot;: [
        {
            &quot;type&quot;: &quot;al&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;name&quot;: &quot;Your own server&quot;,
            &quot;server&quot;: &quot;http://bclts&quot;,
            &quot;serverInstance&quot;: &quot;BC&quot;,
            &quot;authentication&quot;: &quot;UserPassword&quot;,
            &quot;startupObjectId&quot;: 22,
            &quot;startupObjectType&quot;: &quot;Page&quot;,
            &quot;breakOnError&quot;: true,
            &quot;launchBrowser&quot;: true,
            &quot;enableLongRunningSqlStatements&quot;: true,
            &quot;enableSqlInformationDebugger&quot;: true,
            &quot;tenant&quot;: &quot;default&quot;
        }
    ]
}</code></pre><figcaption>The launch.json file content</figcaption></figure><h2 id="publishing-al-extension">Publishing AL Extension</h2><p>You might have noticed that the Hello World application is already there created for us. If you look at the <em>HelloWorld.al</em> file, it extends the Page 22 Customer List to display the &quot;<em>App published: Hello world&quot;</em> message. I don&#x2019;t believe it will break the system, so let&#x2019;s skip testing and just deploy it!</p><p>Now hit <strong>F5</strong> and enter the container user credentials.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-15.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="2000" height="1540" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-15.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-15.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/09/image-15.png 1600w, https://ashirokikh.com/content/images/size/w2400/2020/09/image-15.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>BC Container credentials dialog in VS Code</figcaption></figure><p>The below is a picture of Visual Studio Code in the Debugging mode that it switches to as a result of the previous action.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/09/image-18.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy" width="2000" height="1388" srcset="https://ashirokikh.com/content/images/size/w600/2020/09/image-18.png 600w, https://ashirokikh.com/content/images/size/w1000/2020/09/image-18.png 1000w, https://ashirokikh.com/content/images/size/w1600/2020/09/image-18.png 1600w, https://ashirokikh.com/content/images/size/w2400/2020/09/image-18.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>VS Code attached to a Web Client session</figcaption></figure><p>Now wait a moment and the Web Client should pop up, and the Customer List page should open. And when it opens, we can see our powerful extension at work!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh3.googleusercontent.com/7kdBqvg4t-5GS0eeh_BLzJq0aJf-d2_DDM3MvsPaaFq2UJWZ2aR2fNPsgUfl8rLkuaYax4KDec6CImAeimcRGx9Qo0U29TmbKfGuInTQ31raORkmQu8GPd4tIRumYpGd-zdmmnrz" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>AL Hello World extension message</figcaption></figure><p>Woo-hoo, finally! Now that we have everything working, don&apos;t be shy, apply your creativity and break things! That&apos;s usually the outcome of trying to learn something new for me.</p><h2 id="summary">Summary</h2><p>If your company is running the NAV 2016 or later version, there is probably a Docker image for you to play with. Many of us are running a customized version of NAV/BC, and refreshing (or if you aim for the next version, updating) the knowledge about how the standard logic works adds to the quality of what you&apos;re doing.</p><p>Next time you start a new project, try using a BC Docker image to do some testing on the specific application version to identify the gaps in the standard functionality first. Then do some prototyping to see how it might work out for you (without the rest of customization if possible). And remember, if you break the environment, you&apos;ll get a shiny clean one in 10 minutes! Just remove the container and create a new one.</p><p>I hope you enjoyed reading this article and learned a couple of new tricks. Thank you for reading and until next time! </p><hr><h3 id="additional-tips">Additional Tips</h3><p>Here are some useful commands for working with containers:</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">docker ps # check running containers
docker ps -a # get all containers on the machine
docker stop &lt;container-name&gt; # stop a container, saves memory if not using it
docker start &lt;container-name&gt; # start a stopped (status Exited) container</code></pre><figcaption>Docker commands</figcaption></figure><p>Getting errors similar to the below would mean Docker Desktop is not running.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ashirokikh.com/content/images/2020/05/image.png" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Error when trying to execute a docker command when Docker is not running</figcaption></figure><p>Without a good understanding of the BC Docker environment, we probably shouldn&#x2019;t go much further with the docker commands on BC containers. It&#x2019;s always best to use the available in the BcContainerHelper commands. The wrappers for the commands above are as follows:</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Get-BCContainers # get all BC containers on the machine
Stop-BCContainer &lt;container-name&gt;
Start-BCContainer &lt;container-name&gt; # also provides the start-up logs and BC container specific information like the vsix file location for AL development
</code></pre><figcaption>BcContainerHelper commands</figcaption></figure><p>By the way, the <code>Get-BCContainers</code> command is an alias of the <code>Get-NavContainers</code> command and they can be used interchangeably. Same applies for the rest of the commands.</p><p>Once you&apos;ve finished with a container, just remove it.</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Remove-BCContainer &lt;container-name&gt; # takes care of the Desktop shortcusts too!</code></pre><figcaption>BcContainerHelper command to remove a BC container</figcaption></figure><p>In case there isn&#x2019;t a method for you and you have a good idea, the <a href="https://github.com/microsoft/navcontainerhelper?ref=ashirokikh.com">NavContainerHelper</a> (still has the original name) repository is open to submission of pull requests and contributions!</p><h3 id="additional-links">Additional Links</h3><p>If you&#x2019;re new to AL, <a href="https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-get-started?ref=ashirokikh.com">Getting Started with AL</a> is a great place to look at for further learning AL language and extending Business Central functionality.</p><p>I haven&#x2019;t tried it yet but <a href="https://marketplace.visualstudio.com/items?itemName=martonsagi.al-object-designer&amp;ref=ashirokikh.com">AL Object Designer</a> looks very promising if you&#x2019;re looking for a C/SIDE-like experience, and it comes with some extra nice features.</p><p>More information on Docker is available at the website <a href="https://docs.docker.com/desktop/?ref=ashirokikh.com">https://docs.docker.com/desktop/</a>.</p><p>For more information on BC Container Helper, <a href="https://freddysblog.com/category/navcontainerhelper/?ref=ashirokikh.com">Freddy&#x2019;s blog</a> is where heaps of it can be found.</p><p>In case a BC version 14 or an older NAV version is required, we might need to still need to follow the traditional path and use docker images with <a href="https://github.com/microsoft/navcontainerhelper/blob/master/NavContainerHelper.md?ref=ashirokikh.com">NavContainerHelper</a>. The full list of image tags and other information is available at <a href="https://hub.docker.com/_/microsoft-businesscentral-onprem?ref=ashirokikh.com">https://hub.docker.com/_/microsoft-businesscentral-onprem</a>.</p><p>Dynamics NAV 2016 to 2018 images can be found at <a href="https://hub.docker.com/r/microsoft/dynamics-nav?ref=ashirokikh.com">https://hub.docker.com/r/microsoft/dynamics-nav</a>. The container creation parameters might need to be different for them. Here is a script that I used last time to create a BC 14 container:</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">$navImage = &quot;mcr.microsoft.com/businesscentral/onprem:14.11.41204.0-au&quot;
$containerName = &quot;bc14dev&quot;
$licenseFile = (Join-Path $PSScriptRoot &quot;\license.flf&quot;)
$authenticationType = &apos;UserPassword&apos;

Write-Host &quot;*** Starting $containerName container creation ***&quot; -ForegroundColor Yellow

if ($authenticationType -ne &apos;Windows&apos;){
    $credential = get-credential -Message &quot;Enter NAV Super User credentials&quot;
}

Measure-command {
    New-NavContainer -accept_eula `
                    -accept_outdated `
                    -containerName $containerName `
                    -imageName $navImage `
                    -credential $credential `
                    -memoryLimit 10G `
                    -includeCSide `
                    -doNotExportObjectsToText `
                    -alwaysPull `
                    -updateHosts $true `
                    -auth $authenticationType `
                    -myScripts @() `
                    -navDvdPath &quot;&quot; `
                    -includeTestToolkit `
                    -doNotCheckHealth 
}

IF($licenseFile -ne &quot;&quot;){
    Import-NavContainerLicense -containerName $containerName -licenseFile $licenceFile
}

Write-Host &quot;*** $containerName container creation completed ***&quot; -ForegroundColor Green</code></pre><figcaption>NAV container creation script</figcaption></figure><p>Notice it has the <code>-includeCSide</code> parameter to include our beloved C/SIDE development environment. It also has the <code>-includeTestToolkit</code> to exercise automated unit testing skills. Feel free to try and figure our <a href="https://www.hougaard.com/how-i-do-docker/?ref=ashirokikh.com">the parameter set</a> that suits your needs. </p><hr><h3 id="extra-steps-to-set-up-the-windows-10-dev-environment-vm">Extra Steps to Set Up the Windows 10 Dev Environment VM</h3><p><br>The Windows 10 VM for this walkthrough was created using the Quick Create action in Hyper-V Manager.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lh6.googleusercontent.com/ovEITtNmtftTR1nxE_YCOJFDe3WNfsIWaFwr3r_qiVyi9fPUDZSJIJ0jTjX2JuZ2iYhWEWCXd3fi6f7n_0nu7VsV437FWzb_yTVhG-ZIOkrZ02cgeS2WoyQc13-927gWCHSnfuop" class="kg-image" alt="Walkthrough: AL Extension on Docker Desktop" loading="lazy"><figcaption>Hyper-V Quick Create Dialog</figcaption></figure><p>Then the image (16.10 GB) is downloaded and the machine is created.</p><p>After that, turn it off and enable nested virtualization for it <a href="https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/nested-virtualization?ref=ashirokikh.com">https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/nested-virtualization</a> by running the following command as Administrator:</p><figure class="kg-card kg-code-card"><pre><code class="language-PowerShell">Set-VMProcessor -VMName &lt;VMName&gt; -ExposeVirtualizationExtensions $true</code></pre><figcaption>Enabling nested virtualization command</figcaption></figure><p>Set the amount of memory to at least 8 GB. It should perform better with 10 GB+ of RAM.</p>]]></content:encoded></item></channel></rss>