Preparing Dynamics 365 Business Central Cloud for Automation (Part 1) – Create Azure App Registration

Automation in here means to administrate the Dynamics 365 Business Central (BC) so it will be able to be processed without any human interaction in the BC (a.k.a. scripting)

There are many reasons automation exist: CI/CD is the most common reason. Other reason might be derived from CI/CD, or in my case, to deploy an extension when we have restricted access to BC itself. For instance, it’s not our environment, it’s the client’s.

In order to enable the automation, here are the prerequisite:

  1. An account that can access and create and setup an Azure Apps Registration
  2. User that can setup Azure Entra Registration inside the BC Environment
  3. Of course unrestricted internet, ports, etc. to call BC API later on

I will go through one-by-one. But if you’re impatient, Microsoft already has the article which is accessible in here. I will be just demoing that in more details and graphical way.

Let’s go for the first part, to create an Azure Apps Registration

Create an Azure Apps Registration

Due to many reason, using username and password throughout internet is not highly recommended since very long ago. Many authentication and authorization methods has been developed to reinforce security on that subject. One of those, is using token for the APIs.

BC (cloud) itself don’t manage its authentication and authorization to the internet (on-prem version still has that function though). So for such activities, BC relies on Azure Entra ID to do the authentication and authorization, later on Azure will communicate with BC whether a particular activity is allowed to be run in BC.

Thus you’ll need to have an account to create an Azure Apps Registration on your Azure Portal. Apps Registration itself should be free of charge, even if you don’t have any Azure subscription (tested in my demo tenant).

So, let’s first create a new Azure Apps Registration. It’s highly recommended to create a new Azure Apps Registration FOR EACH BC environment (productions, sandboxes). This one, I will show to create just one for one exact environment

Go to https://portal.azure.com with your account, and find “Apps Registration”

Let’s create a new Apps Registration

For the next screen, pay attention on #1 and #2. If you have multiple tenants, for instance @domain1.com and @domain2.com, you will need to select at least option 2 if you want to be able to use single credential for both tenants. For #2, you have to fill it with that URL https://businesscentral.dynamics.com/OAuthLanding.htm as we will need it later on

After it’s created, we can configure the Apps Registration settings. Follow #1 until #3

Now, depending of what purpose of your APIs will be, you must select the correct permission for that API. In my case, I would just need the API to be able to update extension, so I select only the automation. Yours might be different.

Remember to give admin consent too, just in case. We will need it again later on next part from BC.

Once it’s green, we’re good to set up the client secret

We will need at least one, unless you plan to share the secret with someone else, which later you want to be able to control it

Name anything you want and set the expired date

Now, copy only the value and leave the secret ID

We will also need to take notes of these from Apps Registration Overview for the next part

Now, we have successfully setup the Azure Apps Registration, we can go to the next part

Preparing Dynamics 365 Business Central Cloud for Automation (Part 3) – Accessing The APIs

For the last part, this subject might be varied depending on your purposes. In my case, I will need to push an extension update without having direct access to the BC environment.

There are 4 APIs to serve my purpose:

  1. To list all installed extensions in an environment, so I can now at least the status and latest version my extension which is installed
  2. To update the extension, obviously
  3. To monitor the extension installed, whether it’s successfully installed or a failure happens
  4. (NOT AVAILABLE YET) Get details on the extension installation progress, ESPECIALLY when a failure happens — this last one has not provided by Microsoft yet

Microsoft has listed all those APIs that can be accessed after we have done Part 1 and Part 2 in here. For the sake of brevity, these all the endpoints that I will use

  1. https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/api/dynamics_extension_get
  2. https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/api/dynamics_extensionupload_update (YES, you just only need to patch even if you haven’t installed the extension previously)
  3. https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/api/dynamics_extensiondeploymentstatus_get
  4. (NOT AVAILABLE YET) I’m currently asking whether they already have one https://github.com/MicrosoftDocs/dynamics365smb-devitpro-pb/issues/3227

You can hit those APIs with practically with any tools, in here I will just use Postman to do so. I will share the gits at the very bottom, you’ll have to import those 2 files into your Postman.

You will need to set up the environment first, use everything that I have mentioned before from my Part 1 and Part 2 to fill up the environment variables

Then you can test whether the variables that you have inputted are correct by calling the API List. If it’s all good, you’re good to go.

{
"id": "224d2a2c-ab55-4908-879a-3f8f9f97eb3b",
"name": "BCEnviName",
"values": [
{
"key": "AzureAppId",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "AzureClientSecret",
"value": "",
"type": "secret",
"enabled": true
},
{
"key": "BCTenantId",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "BCEnviName",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "CompanyId",
"value": "",
"type": "default",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2024-02-08T15:33:46.196Z",
"_postman_exported_using": "Postman/10.22.13"
}
{
"info": {
"_postman_id": "9c95eb06-8cce-45cc-8d19-b22e123c2982",
"name": "BC APIs",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "31724588"
},
"item": [
{
"name": "API List",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://api.businesscentral.dynamics.com/v2.0/{{BCTenantId}}/{{BCEnviName}}/api/v2.0",
"protocol": "https",
"host": [
"api",
"businesscentral",
"dynamics",
"com"
],
"path": [
"v2.0",
"{{BCTenantId}}",
"{{BCEnviName}}",
"api",
"v2.0"
]
}
},
"response": []
},
{
"name": "Get Extension List",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://api.businesscentral.dynamics.com/v2.0/{{BCTenantId}}/{{BCEnviName}}/api/microsoft/automation/v1.0/companies({{CompanyId}})/extensions",
"protocol": "https",
"host": [
"api",
"businesscentral",
"dynamics",
"com"
],
"path": [
"v2.0",
"{{BCTenantId}}",
"{{BCEnviName}}",
"api",
"microsoft",
"automation",
"v1.0",
"companies({{CompanyId}})",
"extensions"
]
}
},
"response": []
},
{
"name": "Upload Extension",
"request": {
"method": "PATCH",
"header": [
{
"key": "Content-Type",
"value": "application/octet-stream",
"type": "text"
},
{
"key": "If-Match",
"value": "*",
"type": "text"
}
],
"body": {
"mode": "file",
"file": {
"src": ""
}
},
"url": {
"raw": "https://api.businesscentral.dynamics.com/v2.0/{{BCTenantId}}/{{BCEnviName}}/api/microsoft/automation/v1.0/companies({{CompanyId}})/extensionUpload(0)/content",
"protocol": "https",
"host": [
"api",
"businesscentral",
"dynamics",
"com"
],
"path": [
"v2.0",
"{{BCTenantId}}",
"{{BCEnviName}}",
"api",
"microsoft",
"automation",
"v1.0",
"companies({{CompanyId}})",
"extensionUpload(0)",
"content"
]
}
},
"response": []
},
{
"name": "Get Extension Update Status",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://api.businesscentral.dynamics.com/v2.0/{{BCTenantId}}/{{BCEnviName}}/api/microsoft/automation/v1.0/companies({{CompanyId}})/extensionDeploymentStatus",
"protocol": "https",
"host": [
"api",
"businesscentral",
"dynamics",
"com"
],
"path": [
"v2.0",
"{{BCTenantId}}",
"{{BCEnviName}}",
"api",
"microsoft",
"automation",
"v1.0",
"companies({{CompanyId}})",
"extensionDeploymentStatus"
]
}
},
"response": []
}
],
"auth": {
"type": "oauth2",
"oauth2": [
{
"key": "authUrl",
"value": "https://login.windows.net/{{BCTenantId}}/oauth2/authorize?resource=https://api.businesscentral.dynamics.com",
"type": "string"
},
{
"key": "accessTokenUrl",
"value": "https://login.windows.net/{{BCTenantId}}/oauth2/token?resource=https://api.businesscentral.dynamics.com",
"type": "string"
},
{
"key": "clientId",
"value": "{{AzureAppId}}",
"type": "string"
},
{
"key": "clientSecret",
"value": "{{AzureClientSecret}}",
"type": "string"
},
{
"key": "redirect_uri",
"value": "https://businesscentral.dynamics.com/OAuthLanding.htm",
"type": "string"
},
{
"key": "grant_type",
"value": "authorization_code",
"type": "string"
},
{
"key": "tokenName",
"value": "AuthToken",
"type": "string"
},
{
"key": "scope",
"value": "https://api.businesscentral.dynamics.com/.default",
"type": "string"
},
{
"key": "addTokenTo",
"value": "header",
"type": "string"
}
]
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
]
}

Enjoy.

Preparing Dynamics 365 Business Central Cloud for Automation (Part 2) – Setup Microsoft Entra Application in BC

Unfortunately, to be able to do the automation in BC, we still need to setup something too in there. Hence the second prerequisite: an admin account in BC to setup “Microsoft Entra Application”. If you don’t have admin access to BC, you’ll need someone to setup it for you.

In short: Azure Apps Registration enables us to authenticate and authorize access to the BC APIs, but Azure itself needs to communicate with BC in order to do so.

Go to Microsoft Entra Applications

There are several things that we need to setup

From my first part, enter the Apps Registration Client ID in #1 and what ever description it is. Then set the State to Enabled. Then a window will appear confirm there will be a user created for this, just press Yes

Hit the #2 button, a window popup will appear, you’ll have to grant access with your BC admin account. Press the Accept

Last things, #3 add any permission you need for the APIs that you will use. In my case, I just need the automation. So here’s the last page will be shown.

It would be very helpful too to note at least one company that exists inside your environment, a GUID is more preferable rather than the company name. Because in my case, even though the extension will be used by any companies inside an environment, I still need to define which the company to do so.

Now, we can access the APIs that we need from here.

Extending The Report Selection with Radio Button

Radio button is rarely found in Business Central. It was used more often back in classic NAV where “page” was “form”. It takes too much space so it is rarely found in modern UI, gets replaced by many variations of dropdown. Its main function is to select a single option and dropdown offers same result with less space taken.

However radio button is still can be found when posting directly from Sales Order or Purchase Order, I’m sure anyone uses BC is familiar with this sighting.

One of the surviving radio buttons in Business Central, I guess

Many good reasons why it is left behind, one of them is the dropdown alternative that I have mentioned earlier. The other one is, its options are constructed based on semi-hardcoded comma separated text, thus it’s hard to extend. Our only option is to replace it entirely with new menu, since, again, the menu parameters aren’t configurable.

However, there are times when that old radio button is still acceptable. One of those, is when we have put several reports on Report Selection, and we kinda don’t want user to being ended up running reports sequentially.

Boy, imagine if we put 10 of these reports and we just want to run the 9th one

So I kinda find a way to resurrect that old radio button, to put it back to good use. In here, I hooked (or subscribed) only to Report Selections table events, which they’re enough to fulfil my purpose.

codeunit 50000 PrintSelectionAsOptions
{
    [EventSubscriber(ObjectType::Table, Database::"Report Selections", OnPrintDocumentsOnAfterSelectTempReportSelectionsToPrint, '', false, false)]
    local procedure OnPrintDocumentsOnAfterSelectTempReportSelectionsToPrint(
        var TempReportSelections: Record "Report Selections" temporary
        ; RecordVariant: Variant
    )
    var
        reportCaption: Text;
        counter: Integer;
    begin
        DefaultOption := 1;

        Clear(ReportNames);
        Clear(ReportNamesList);

        TempReportSelections.SetAutoCalcFields("Report Caption");
        TempReportSelections.FindSet();
        //  constructing options
        repeat
            ReportNamesList.Add(TempReportSelections."Report ID", TempReportSelections."Report Caption");
        until TempReportSelections.Next() = 0;

        if ReportNamesList.Count = 0 then
            Error('Error constructing text for options.');

        foreach reportCaption in ReportNamesList.Values do
            ReportNames := ReportNames + reportCaption + ',';

        if ReportNamesList.Count > 1 then begin
            Selection := StrMenu(ReportNames, DefaultOption);
            if Selection = 0 then
                exit;

            counter := 1;
            TempReportSelections.Reset();
            TempReportSelections.FindSet();

            repeat
                if counter = Selection then
                    REPORT.RunModal(TempReportSelections."Report ID", true, false, RecordVariant);
                counter += 1;
            until TempReportSelections.Next() = 0;
        end else
            REPORT.RunModal(TempReportSelections."Report ID", true, false, RecordVariant);

    end;

    [EventSubscriber(ObjectType::Table, Database::"Report Selections", OnBeforePrintDocument, '', false, false)]
    local procedure OnBeforePrintDocument(
        var IsHandled: Boolean
        ; IsGUI: Boolean
    )
    begin
        if not IsGUI then
            exit;

        IsHandled := true;
    end;

    var
        Selection: Integer;
        DefaultOption: Integer;
        ReportNames: Text;
        ReportNamesList: Dictionary of [Integer, Text];
}
Now you don’t have to skip 8 times just to reach the 9th report

Feel free to use the code as you like. Hopefully that would help your situation as it did mine.

Android of mine – Part 3

MIUI was like a glittering gold. Its UI was so refreshing unlike vanilla Android, or the Sense UI. Used to be baked in XDA Forum, the forum of elite-ish of PDA modder, MIUI was originated from China, and no, at that time it still had no affiliation to Xiaomi, until they moved to dedicated forum (the miui.us, for international people).

MIUI really felt like fresh air to the ugly native Android UI (Gingerbread, and Holo, a.k.a. ICS native UI) or bloated UI such Sense or TouchWiz. MIUI v4, and later v5, was available for HTC EVO 3D, and since then it was my favourite UI, well until Material Design came.

The home screen of MIUI v4

From beginning, MIUI already carried concept of “your phone, your style“. Customization was baked deep inside the OS, where you can download and change the OS theme to whatever your liking. The concept now has been manifested in Android OS itself (hello Material You and themed icons).

MIUI borrowed iOS heavily, yet still felt so Android

The UI itself was alive with many animations across activities, along with its transparency effect, it was so glorious compared to other UIs or native one. Even with those animations and effects, the UI itself felt so fluid and smooth, far from lags. That’s where MIUI shine.

Sick transparency effect

But Google was quick to catch up with the Material Design. Learning from mistake that natuve Android UI almost had no clear identity, Material Design pushed so many changes that could make it back on track, to compete properly with heavy bloated UIs out there. But that’s another story.