Batch Request in Business Central APIs

 When calling Business Central APIs you do one operation at a time. For example, you can only insert or modify one customer, or create one sales invoice. With deep inserts, it is possible to create header and lines together, and then you can create multiple lines. But that’s only possible on the line records, you still create one header at a time. With a batch request, it is possible to combine multiple operations in one request. The batch request is submitted as a single HTTP POST request to the $batch endpoint and the body contains a set of operations. From an optimization point of view, batching minimizes the number of requests from an API consumer because you combine multiple requests into one.

Batch URL

The $batch endpoint is available on all API endpoints. For the Business Central SaaS environment you can use this URL:

https://{{baseurl}}/api/v2.0/$batch

Everything I describe here also works for custom APIs. The URL will then look like:

https://{{baseurl}}/api/[publisher]/[group]/[version]/$batch

Request headers

Because the request body will be JSON the Content-Type header of the request must be set to appication/json. If the response should be JSON as well (and you want that!), then the Accept header must also be set to application/json. If you leave the Accept header out, then the response will be multipart/mixed. Here is the basic structure of the request, without the body. I leave out the Authorization header, but you need to add that obviously. ðŸ™‚


POST {{baseurl}}/api/v2.0/$batch Content-Type: application/json Accept: application/json

The requests array must contain one ore more operations, and each of them must contains an id, the method and a URL and optionally also headers and a body. Here is an example of an operation to insert a single journal line.

{

"method": "POST",

"id": "r1",

"url": "companies({{companyId}})/journals({{journalId}})/journalLines",

"headers": {

"Content-Type": "application/json"

},

"body": {

        "accountId": "{{accountId}}",

        "postingDate": "2020-10-20",

"documentNumber": "SALARY2020-10",

"amount": -3250,

"description": "Salary to Bob"

}

}



Let’s take the operation above, to insert a single journal line and compose a batch request that inserts multiple journal lines in one go. The request body looks like:

{

  "requests": [

    {

      "method": "POST",

      "id": "r1",

      "url": "companies({{companyId}})/journals({{journalId}})/journalLines",

      "headers": {

        "Content-Type": "application/json"

      },

      "body": {

        "accountId": "{{accountId}}",

        "postingDate": "2020-10-20",

        "documentNumber": "SALARY2020-12",

        "amount": -3250,

        "description": "Salary to Bob"

      }

    },

    {

      "method": "POST",

      "id": "r2",

      "url": "companies({{companyId}})/journals({{journalId}})/journalLines",

      "headers": {

        "Content-Type": "application/json"

      },

      "body": {

        "accountId": "{{accountId}}",

        "postingDate": "2020-10-20",

        "documentNumber": "SALARY2020-12",

        "amount": -3500,

        "description": "Salary to John"

      }

    },

    {

      "method": "POST",

      "id": "r3",

      "url": "companies({{companyId}})/journals({{journalId}})/journalLines",

      "headers": {

        "Content-Type": "application/json"

      },

      "body": {

        "accountId": "{{accountId2}}",

        "postingDate": "2020-10-20",

        "documentNumber": "SALARY2020-12",

        "amount": 6750,

        "description": "Salaries December 2020"

      }

    }

  ]

}

As you can see, each operation has a corresponding result in the response, identified by the id. You should always use the id of the individual operation to find the corresponding result. Don’t assume that the results will always be in the same order as the request! They may seem to be in the same order, but the OData standard describes that the results can be in any order.

Each operation result has a status, which is the HTTP status that you would normally get for a single request. It includes the response headers and response body of the individual operation. The response of the batch request itself will always have status 200 if the server was able to read the batch request. Even if the batch request has operations that couldn’t be processed because of an error condition, the batch response status will still be 200. So don’t only look at the response status of the batch request, you need to read the response body to find out the results of each operation. Only when the batch request body is malformed, or you are not authorized, then the whole batch request will fail and you will get a status 500 (malformed batch request) or status 401 (not authorized).

So far it’s really simple, isn’t it? In the next blog posts in this series, I will cover topics like:

  • What happens if an error occurs in one of the requests
  • Process a batch as one big transaction
  • Combine multiple operations types, like POST and GET
  • Define the order of operations
  • Reduce the size of the response payload



Continue Reading...

Function for Split Text With Maximum Characters Without Damaging Words – AL / C/AL

 local procedure TrySplitTextWithoutDamagingWords(FullText: Text; MaximumCaractorLength:Integer; var MaximumText: Text; var RemainingText: Text) IsRemainingToSplit: Boolean

    var

        SpcPos: Integer;

        MaxSpcPos: Integer;

        CheckText: Text;

    begin

        FullText := DelChr(FullText, '<>', ' ');

        if (FullText = '') or (MaximumCaractorLength = 0) or (StrLen(FullText) <= MaximumCaractorLength) then begin

            MaximumText := FullText;

            RemainingText := '';

            exit(false);

        end;

        MaximumText := CopyStr(FullText, 1, MaximumCaractorLength);

        RemainingText := CopyStr(FullText, MaximumCaractorLength + 1, StrLen(FullText));

        if CopyStr(FullText, MaximumCaractorLength+1, 1)<>' 'then begin

            CheckText := MaximumText;

            repeat begin

                SpcPos := StrPos(CheckText, ' ');

                MaxSpcPos += SpcPos;

                CheckText := CopyStr(CheckText, SpcPos + 1, MaximumCaractorLength);

            end until SpcPos = 0;

            if MaxSpcPos > 0 then begin

                MaximumText := CopyStr(FullText, 1, MaxSpcPos - 1);

                RemainingText := CopyStr(FullText, MaxSpcPos + 1, StrLen(FullText));

            end;

        end;

        exit(StrLen(RemainingText) > MaximumCaractorLength);

    end;

How to use the function?

There are four parameters and a boolean type return value.

  1. Parameter – FullText:
    The text that you want to split. The data type is Text.
  2. Parameter – MaximumCaractorLength:
    Maximum length to split. The data type is Integer.
  3. Parameter – MaximumText:
    The var type parameter that will be assigned split text. The data type is Text.
  4. Parameter – RemainingText:
    The var type parameter that will be assigned remaining text. The data type is Text.
  5. Return Value – IsRemainingToSplit:
    If there is a remaining text exceeding the maximum length it returns true. The data type is Boolean.

Example 1: Single split:

I create three local variables (FullText: Text, SplitText: Text, and RemainingText: Text) and pass them as follows (The maximum character length is 26).

FullText:='NAVUSER is the best site for learning Microsoft Dynamics Navision and Dynamics 365 Business Central.';

TrySplitTextWithoutDamagingWords(FullText,26,SplitText,RemainingText);

Message(SplitText);

FullText:='NAVUSER is the best site for learning Microsoft Dynamics Navision and Dynamics 365 Business Central.';

RemainingText:=FullText;

repeat

     TrySplitTextWithoutDamagingWords(RemainingText,26,SplitText,RemainingText);

     Message(SplitText);

until RemainingText='';

Continue Reading...

Send Email – Dynamic Contents


Continue Reading...

Isolated Storage instead of Service Password table

Storing passwords directly in a table is a bad idea from a security point of view. For secure storage of passwords, the Service Password system table was previously used. In Business Central 15.0, it has been removed.

We are offered to use a new data type - Isolated Storage. Here we'll look at a practical example of using this functionality to store passwords securely.
Suppose that we have a table and a setup page, the user should be able to save the password on the page so that it is used by the system somewhere in the process.To do this, we will write three main procedures: SetPassword()GetPassword()GetStorageKey(). Pay attention to the Acces property, it has the value Internal, which will not allow other extensions to access the password. We will also need a secret key in the GUID format by which the password will be stored. You can generate a key in any Online GUID Generator.

In our example, we will use Module DataScope.

table 50000 "General Setup"
{
    DataClassification = CustomerContent;
    Caption = 'General Setup';
    Access = Internal;
    fields
    {
        field(50000; "Primary Key"; Code[10])
        {
            DataClassification = CustomerContent;
            Caption = 'Primary Key';
        }
    }

    keys
    {
        key(PK; "Primary Key")
        {
            Clustered = true;
        }
    }

    procedure SetPassword(NewPassword: Text)
    var
        EncryptionManagement: Codeunit "Cryptography Management";
    begin
        //Delete old password if exist
        if IsolatedStorage.Contains(GetStorageKey(), DataScope::Module) then
            IsolatedStorage.Delete((GetStorageKey(), DataScope::Module));
        //Encrypt password if possible
        if EncryptionManagement.IsEncryptionEnabled() and
         EncryptionManagement.IsEncryptionPossible() then
            NewPassword := EncryptionManagement.Encrypt(NewPassword);
        //Set new password by storage key
        IsolatedStorage.set(GetStorageKey(), NewPassword, DataScope::Module);
    end;

    procedure GetPassword(): Text
    var
        EncryptionManagement: Codeunit "Cryptography Management";
        PasswordTxt: Text;
    begin
        //Check if password exist by storage key
        if IsolatedStorage.Contains(GetStorageKey(), DataScope::Module) then begin
            //Get exist password
            IsolatedStorage.Get(GetStorageKey(), DataScope::Module, PasswordTxt);
            //Decrypt password if possible
            if EncryptionManagement.IsEncryptionEnabled() and
            EncryptionManagement.IsEncryptionPossible() then
                PasswordTxt := EncryptionManagement.Decrypt(PasswordTxt);
            //Return password
            exit(PasswordTxt);
        end;
    end;

    local procedure GetStorageKey(): Text
    var
        StorageKeyTxt: Label '25edcb48-11b9-4e7d-8645-2c95b3156092', Locked = true;
    begin
        exit(StorageKeyTxt);
    end;
}
Besides, we need a page where the user can specify a password.
page 50000 "General Setup"
{
    PageType = Card;
    ApplicationArea = All;
    UsageCategory = Administration;
    SourceTable = "General Setup";
    Caption = 'General Setup';

    layout
    {
        area(Content)
        {
            group(General)
            {
                field(PasswordTxt; PasswordTxt)
                {
                    ApplicationArea = All;
                    ExtendedDatatype = Masked;
                    Caption = 'Password';
                    trigger OnValidate()
                    begin
                        SetPassword(PasswordTxt);
                        Commit();
                    end;
                }
            }
        }
    }

    trigger OnOpenPage()
    begin
        Reset();
        if not Get() then begin
            Init();
            Insert();
            SetPassword('');
        end else
            PasswordTxt := '***';
    end;

    var
        PasswordTxt: Text;

} 

Continue Reading...

Isolated Storage -Business Central

  procedure SetAPIKey(NewAPIKey: Text)

var EncryptionManagement: Codeunit "Cryptography Management"; begin if IsolatedStorage.Contains(GetStorageKey(), DataScope::Module) then IsolatedStorage.Delete((GetStorageKey())); if EncryptionManagement.IsEncryptionEnabled() and EncryptionManagement.IsEncryptionPossible() then NewAPIKey := EncryptionManagement.Encrypt(NewAPIKey); IsolatedStorage.set(GetStorageKey(), NewAPIKey, DataScope::Module); end; procedure GetAPIKey(): Text var EncryptionManagement: Codeunit "Cryptography Management"; APIKey: Text; begin if IsolatedStorage.Contains(GetStorageKey(), DataScope::Module) then begin IsolatedStorage.Get(GetStorageKey(), DataScope::Module, APIKey); if EncryptionManagement.IsEncryptionEnabled() and EncryptionManagement.IsEncryptionPossible() then APIKey := EncryptionManagement.Decrypt(APIKey); exit(APIKey); end; end; local procedure GetStorageKey(): Text begin exit(SystemId); end;
Continue Reading...