Business Central Azure Functions (Part 2)

Neil HaddleyDecember 16, 2025

Creating the Business Central Extension

AzureMicrosoft DynamicsBusiness Centralazurebusiness centralazure functions

Creating the Business Central Extension

Azure Functions App used to generate Customer QR Code

Azure Functions App used to generate Customer QR Code

project files

TEXT
1ALProjectQRCodeGenerator/
2├── .vscode/
3├── .alpackages/
4├── .git/
5├── .snapshots/
6├── app.json
7├── Customer Card QR Extension.al
8├── Customer QR Code FactBox.al
9├── QR Code Helper.al
10├── QR Code Media.al
11└── Neil Haddley_ALProjectQRCodeGenerator_1.0.0.0.app

Summary: ALProjectQRCodeGenerator

Overview

A Business Central extension that automatically generates and displays QR codes for customer records using an Azure Function integration.


Architecture Flow
CODE
1Customer Card → FactBox → Helper → Azure Function → MediaSet Storage → Display

1. QR Code Media.al (Table 50100)

Purpose: Persistent storage for QR code images

AL
1table 50100 "QR Code Media"
2{
3    fields
4    {
5        "Customer No." (Code[20])      // Primary key, links to Customer
6        "Media ID" (Media)             // Legacy binary storage field
7        "QR Code Image" (MediaSet)     // Active field for UI display
8        "Created DateTime" (DateTime)   // Auto-timestamp on insert
9    }
10    
11    trigger OnInsert() 
12    {
13        "Created DateTime" := CurrentDateTime;  // Auto-populate timestamp
14    }
15}

2. QR Code Helper.al (Codeunit 50100)

Purpose: Business logic for QR code generation and storage

CODE
11. Validate customer exists
22. Build QR text content (customer number + name)
33. Construct API URL with encoded parameters
44. Detect response format (base64 vs binary)
55. Call Azure Function via HttpClient
66. Convert response to MediaSet
77. Store in database
88. Return MediaId GUID

GetCustomerQRText() - Content Builder

- Retrieves company info

- Formats: CUSTOMER:[No.]|NAME:[Name]

- Commented option: Direct URL to BC customer card

StoreQRCodeImageFromBase64() - Base64 Handler

- Extracts base64 data from data:image/png;base64,... URI

- Uses Base64 Convert codeunit

- Converts to binary stream via Temp Blob

- Imports to MediaSet with .png filename

StoreQRCodeImage() - Binary Handler

- Direct stream import from HTTP response

- Used for APIs returning raw PNG bytes

IsBase64Response() - Format Detector

- Checks if URL contains 'azurewebsites'

- Returns true for Azure Functions (base64)

- Returns false for public APIs (binary)

EncodeUrl() - URL Encoding

- Uses Type Helper.UriEscapeDataString()

- Properly encodes special characters for query parameters


3. Customer QR Code FactBox.al (Page 50100)

Purpose: UI component displaying QR code in Customer Card

AL
1page 50100 "Customer QR Code FactBox"
2{
3    PageType = CardPart;
4    SourceTable = "QR Code Media";
5    
6    layout {
7        field("QR Code Image")    // MediaSet displays as image
8        label("Scan to view...")  // User guidance text
9    }
10    
11    trigger OnOpenPage() {
12        if Rec.IsEmpty() then
13            GenerateQRCode();  // Auto-generate if missing
14    }
15}

Logic:

- Checks if QR code exists on page open

- Calls GenerateQRCodeForCustomer() if missing

- Refreshes page to display new image


4. Customer Card QR Extension.al (PageExtension 50100)

Purpose: Integrates FactBox into standard Customer Card

AL
1pageextension 50100 extends "Customer Card"
2{
3    layout {
4        addfirst(factboxes) {
5            part(QRCodeFactBox; "Customer QR Code FactBox") {
6                SubPageLink = "Customer No." = FIELD("No.");
7            }
8        }
9    }
10}

Integration:

- Adds FactBox to top of factboxes area

- Links FactBox to current customer via SubPageLink

- No code modifications to base page


5. app.json

Configuration:

- Dependencies: Base Application (27.0), System Application (27.0)

- Object Range: 50100-50149

- Runtime: 16.0 with NoImplicitWith

- Publisher: Neil Haddley


Technical Highlights

Dual Format Support: Handles both base64 and binary image responses

Auto-Generation: Creates QR codes on-demand when missing

Clean Separation: Helper handles logic, FactBox handles UI

Modern BC Patterns: Uses HttpClient (not deprecated Azure Functions codeunits)

MediaSet Storage: Proper image handling for BC UI rendering

URL Encoding: Safely handles special characters in customer data


Data Flow Example
CODE
1User opens Customer "C001" 
23FactBox checks QR Code Media for "C001"
45Not found → Calls GenerateQRCodeForCustomer("C001")
67Helper builds: "CUSTOMER:C001|NAME:Contoso Ltd"
89Calls: https://azure-function.../api/QRCodeGenerator?text=CUSTOMER%3AC001...
1011Receives: data:image/png;base64,iVBORw0KGgoAAAA...
1213Converts base64 → binary stream → MediaSet
1415Stores in QR Code Media table
1617FactBox displays QR code image
1819User scans with phone → Sees customer info

app.json

JSON
1{
2  "id": "d3c72ccb-3945-46b3-8e4e-b784aaf1bd69",
3  "name": "ALProjectQRCodeGenerator",
4  "publisher": "Neil Haddley",
5  "version": "1.0.0.0",
6  "brief": "",
7  "description": "",
8  "privacyStatement": "",
9  "EULA": "",
10  "help": "",
11  "url": "",
12  "logo": "",
13  "dependencies": [
14    {
15      "id": "437dbf0e-84ff-417a-965d-ed2bb9650972",
16      "name": "Base Application",
17      "publisher": "Microsoft",
18      "version": "27.0.0.0"
19    },
20    {
21      "id": "63ca2fa4-4f03-4f2b-a480-172fef340d3f",
22      "name": "System Application",
23      "publisher": "Microsoft",
24      "version": "27.0.0.0"
25    }
26  ],
27  "screenshots": [],
28  "platform": "1.0.0.0",
29  "application": "27.0.0.0",
30  "idRanges": [
31    {
32      "from": 50100,
33      "to": 50149
34    }
35  ],
36  "resourceExposurePolicy": {
37    "allowDebugging": true,
38    "allowDownloadingSource": true,
39    "includeSourceInSymbolFile": true
40  },
41  "runtime": "16.0",
42  "features": [
43    "NoImplicitWith"
44  ]
45}

Customer Card QR Extension.al

CODE
1pageextension 50100 "Customer Card QR Extension" extends "Customer Card"
2{
3    layout
4    {
5        addfirst(factboxes)
6        {
7            part(QRCodeFactBox; "Customer QR Code FactBox")
8            {
9                ApplicationArea = All;
10                SubPageLink = "Customer No." = FIELD("No.");
11            }
12        }
13    }
14}

Customer QR Code FactBox.al

CODE
1page 50100 "Customer QR Code FactBox"
2{
3    PageType = CardPart;
4    SourceTable = "QR Code Media";
5    Caption = 'Customer QR Code';
6
7    layout
8    {
9        area(Content)
10        {
11            group(QRCodeGroup)
12            {
13                ShowCaption = false;
14                field(QRCodeImage; Rec."QR Code Image")
15                {
16                    ApplicationArea = All;
17                    ShowCaption = false;
18                    Editable = false;
19                    ToolTip = 'QR Code for customer';
20                }
21                label(Description)
22                {
23                    ApplicationArea = All;
24                    Caption = 'Scan to view customer details';
25                    Style = Subordinate;
26                }
27            }
28        }
29    }
30
31    var
32        QRCodeHelper: Codeunit "QR Code Helper";
33
34    trigger OnAfterGetRecord()
35    begin
36        // Record already loaded
37    end;
38
39    trigger OnOpenPage()
40    begin
41        // Generate QR code if it doesn't exist
42        if Rec.IsEmpty() then
43            GenerateQRCode();
44    end;
45
46    local procedure GenerateQRCode()
47    var
48        Customer: Record Customer;
49    begin
50        if Customer.Get(Rec."Customer No.") then
51            QRCodeHelper.GenerateQRCodeForCustomer(Customer."No.");
52
53        // Refresh the page to show the new QR code
54        if Rec.Get(Rec."Customer No.") then;
55    end;
56}

QR Code Helper.al

CODE
1codeunit 50100 "QR Code Helper"
2{
3    // This function generates a QR code for a customer and stores it
4    procedure GenerateQRCodeForCustomer(CustomerNo: Code[20]): Guid
5    var
6        Customer: Record Customer;
7        HttpClient: HttpClient;
8        HttpResponse: HttpResponseMessage;
9        ResponseText: Text;
10        InStr: InStream;
11        FunctionUrl: Text;
12        QRCodeText: Text;
13        ImageMediaId: Guid;
14        UseBase64: Boolean;
15    begin
16        // 1. Get the customer record
17        if not Customer.Get(CustomerNo) then
18            exit;
19
20        // 2. Build the text to encode in the QR code
21        QRCodeText := GetCustomerQRText(Customer);
22
23        // 3. Build the full URL with encoded query parameter
24        FunctionUrl := GetFunctionUrl() + EncodeUrl(QRCodeText);
25        UseBase64 := IsBase64Response(FunctionUrl);
26
27        // 4. Call the API to generate QR code
28        if not HttpClient.Get(FunctionUrl, HttpResponse) then
29            Error('Failed to call QR code generation service');
30
31        if not HttpResponse.IsSuccessStatusCode() then
32            Error('Failed to generate QR code. Status: %1', HttpResponse.HttpStatusCode());
33
34        // 5. Get the response content
35        if UseBase64 then begin
36            if not HttpResponse.Content.ReadAs(ResponseText) then
37                Error('Failed to read response content');
38            ImageMediaId := StoreQRCodeImageFromBase64(CustomerNo, ResponseText);
39        end else begin
40            if not HttpResponse.Content.ReadAs(InStr) then
41                Error('Failed to read response content');
42            ImageMediaId := StoreQRCodeImage(CustomerNo, InStr);
43        end;
44
45        exit(ImageMediaId);
46    end;
47
48    local procedure EncodeUrl(TextToEncode: Text): Text
49    var
50        TypeHelper: Codeunit "Type Helper";
51    begin
52        exit(TypeHelper.UriEscapeDataString(TextToEncode));
53    end;
54
55    local procedure GetCustomerQRText(Customer: Record Customer): Text
56    var
57        CompanyInfo: Record "Company Information";
58        BaseUrl: Text;
59    begin
60        // Customize this based on what you want in the QR code
61        if CompanyInfo.Get() then
62            BaseUrl := CompanyInfo."Home Page"; // Or your Business Central base URL
63
64        // Example 1: URL to the customer card in Business Central
65        // exit(StrSubstNo('%1?page=21&bookmark=Customer.''No.''=%2', BaseUrl, Customer."No."));
66
67        // Example 2: Simple customer information
68        exit(StrSubstNo('CUSTOMER:%1|NAME:%2', Customer."No.", Customer.Name));
69    end;
70
71    local procedure StoreQRCodeImageFromBase64(CustomerNo: Code[20]; Base64DataUri: Text): Guid
72    var
73        QRCodeMedia: Record "QR Code Media";
74        Base64Convert: Codeunit "Base64 Convert";
75        TempBlob: Codeunit "Temp Blob";
76        InStr: InStream;
77        OutStr: OutStream;
78        Base64Data: Text;
79        PrefixPos: Integer;
80    begin
81        // Check if record already exists
82        if QRCodeMedia.Get(CustomerNo) then
83            QRCodeMedia.Delete();
84
85        // Extract base64 data from data URI
86        // Format: data:image/png;base64,iVBORw0KGgo...
87        PrefixPos := StrPos(Base64DataUri, ',');
88        if PrefixPos > 0 then
89            Base64Data := CopyStr(Base64DataUri, PrefixPos + 1)
90        else
91            Base64Data := Base64DataUri;
92
93        // Convert base64 to stream
94        TempBlob.CreateOutStream(OutStr);
95        Base64Convert.FromBase64(Base64Data, OutStr);
96        TempBlob.CreateInStream(InStr);
97
98        // Create a new record
99        QRCodeMedia.Init();
100        QRCodeMedia."Customer No." := CustomerNo;
101
102        // Import into MediaSet field
103        QRCodeMedia."QR Code Image".ImportStream(InStr, 'QRCode.png');
104        QRCodeMedia.Insert(true);
105
106        exit(QRCodeMedia."QR Code Image".MediaId());
107    end;
108
109    local procedure StoreQRCodeImage(CustomerNo: Code[20]; InStr: InStream): Guid
110    var
111        QRCodeMedia: Record "QR Code Media";
112    begin
113        // Check if record already exists
114        if QRCodeMedia.Get(CustomerNo) then
115            QRCodeMedia.Delete();
116
117        // Create a new record in our custom media table
118        QRCodeMedia.Init();
119        QRCodeMedia."Customer No." := CustomerNo;
120        QRCodeMedia."QR Code Image".ImportStream(InStr, 'QRCode.png');
121        QRCodeMedia.Insert(true);
122
123        exit(QRCodeMedia."QR Code Image".MediaId());
124    end;
125
126    local procedure GetFunctionUrl(): Text
127    begin
128
129        // Your Azure Function that returns base64 data URI
130        exit('https://qrcode-generator-function-e5c2d8dwcabsbbfe.eastus-01.azurewebsites.net/api/QRCodeGenerator?text=');
131
132
133        // Alternative: Using public QR API (returns binary PNG directly)
134        //exit('https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=');
135    end;
136
137    local procedure IsBase64Response(Url: Text): Boolean
138    begin
139        // Returns true if the configured API returns base64 data URI
140        // Azure Functions typically return base64, public APIs return binary
141        exit(StrPos(Url, 'azurewebsites') > 0);
142    end;
143
144    local procedure GetFunctionKey(): Text
145    begin
146        // IMPORTANT: Use Isolated Storage or Azure Key Vault integration in production
147        exit('your-actual-function-key-here');
148    end;
149}

QR Code Media.al

CODE
1table 50100 "QR Code Media"
2{
3    DataClassification = ToBeClassified;
4
5    fields
6    {
7        field(1; "Customer No."; Code[20])
8        {
9            TableRelation = Customer;
10        }
11        field(2; "Media ID"; Media)
12        {
13            // This field stores the actual image
14            Caption = 'QR Code Image';
15        }
16        field(3; "QR Code Image"; MediaSet)
17        {
18            Caption = 'QR Code';
19        }
20        field(4; "Created DateTime"; DateTime)
21        {
22            Editable = false;
23        }
24    }
25
26    keys
27    {
28        key(PK; "Customer No.")
29        {
30            Clustered = true;
31        }
32    }
33
34    trigger OnInsert()
35    begin
36        "Created DateTime" := CurrentDateTime;
37    end;
38}

The error message "The request was blocked by the runtime to prevent accidental use of production services" indicates that outbound HTTP calls are blocked by default in your Business Central environment, typically a sandbox or a copied production environment. To resolve this, you need to manually enable the setting for your extension.

Allow HttpClient Requests

Allow HttpClient Requests