It is good practice to have some audit log of what uses do in the application. Some versions ago Microsoft introduced the Change Log to log data changes. How about logging an action execution?
One of the built in solutions in Business Central can be used to solve this. We now have the Activity Log (Table 710).
To use the Activity Log we need to have a record to attach the activity log to. All our Apps have a Setup table that usually only have one record. I like to attach my Activity Log to that record.
To show the Activity Log from that record you can add this action to that record’s page.
action("ActivityLog")
{
ApplicationArea = All;
Caption = 'Activity Log';
Image = Log;
Promoted = true;
PromotedCategory = Process;
PromotedOnly = true;
Scope = "Page";
ToolTip = 'See the data activities for this App.';
trigger OnAction()
var
ActivityLog: Record "Activity Log";
begin
ActivityLog.ShowEntries(Rec);
end;
}
The logging part can be something like this.
local procedure LogActivity(ADVUpgradeProjTable: Record "ADV Upgrade Project Table"; Context: Text[30])
var
ActivityLog: Record "Activity Log";
Status: Option Success,Failed;
begin
if ADVUpgradeProject."App Package Id" <> ADVUpgradeProjTable."App Package Id" then begin
ADVUpgradeProject.SetRange("App Package Id", ADVUpgradeProjTable."App Package Id");
ADVUpgradeProject.FindFirst();
end;
ActivityLog.LogActivity(
ADVUpgradeProject,
Status::Success,
Context,
StrSubstNo('%1', ADVUpgradeProjTable."Data Upgrade Method"),
StrSubstNo('%1 (%2)', ADVUpgradeProjTable."App Table Name", ADVUpgradeProjTable."App Table Id"));
end;
We also have the possibility to log details. Both a text value and also from an in-stream.
In Business Central we have information about the execution context. I pass that execution context into the LogActivity. This gives me information on the session that is executing the code.
local procedure GetExecutionContext(): Text[30]
var
SessionContext: ExecutionContext;
begin
SessionContext := Session.GetCurrentModuleExecutionContext();
case SessionContext of
SessionContext::Install:
exit(CopyStr(InstallationMsg, 1, 30));
SessionContext::Upgrade:
exit(CopyStr(UpgradeMsg, 1, 30));
SessionContext::Normal:
exit(CopyStr(UserContextMsg, 1, 30));
end;
end;
var
InstallationMsg: Label 'App Installation';
UpgradeMsg: Label 'App Upgrade';
UserContextMsg: Label 'Started by user';
Using this logic we can log all execution during install, upgrade and normal user cases. If we need information on the variables we can log them into the detailed information using either JSON or XML.
When we design and write our code we need to think about performance.
We have been used to thinking about database performance, using FindFirst(), FindSet(), IsEmpty() where appropriate.
We also need to think about performance when we create our subscriber Codeunits.
Let’s consider this Codeunit.
codeunit 50100 MySubscriberCodeunit
{
trigger OnRun()
begin
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', true, true)]
local procedure MyProcedure(var SalesHeader: Record "Sales Header")
begin
Message('I am pleased that you called.');
end;
}
Every time any user posts a sales document this subscriber will be executed.
Executing this subscriber will need to load an instance of this Codeunit into the server memory. After execution the Codeunit instance is trashed.
The resources needed to initiate an instance of this Codeunit and trash it again, and doing that for every sales document being posted are a waste of resources.
If we change the Codeunit and make it a “Single Instance”.
codeunit 50100 MySubscriberCodeunit
{
SingleInstance = true;
trigger OnRun()
begin
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', true, true)]
local procedure MyProcedure(var SalesHeader: Record "Sales Header")
begin
Message('I am pleased that you called.');
end;
}
What happens now is that Codeunit only has one instance for each session. When the first sales document is posted then the an instance of the Codeunit is created and kept in memory on the server as long as the session is alive.
This will save the resources needed to initialize an instance and tear it down again.
Making sure that our subscriber Codeunits are set to single instance is even more important for subscribers to system events that are frequently executed.
Note that a single instance Codeunit used for subscription should not have any global variables, since the global variables are also kept in memory though out the session lifetime.
Make sure that whatever is executed inside a single instance subscriber Codeunit is executed in a local procedure. The variables inside a local procedure are cleared between every execution, also in a single instance Codeunit.
codeunit 50100 MySubscriberCodeunit
{
SingleInstance = true;
trigger OnRun()
begin
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', true, true)]
local procedure MyProcedure(var SalesHeader: Record "Sales Header")
begin
ExecuteBusinessLogic(SalesHeader);
end;
local procedure ExecuteBusinessLogic(SalesHeader: Record "Sales Header")
var
Customer: Record Customer;
begin
Message('I am pleased that you called.');
end;
}
If your custom code executes every time that the subscriber is executed then I am fine with having that code in a local procedure inside the single instance Codeunit.
Still, I would suggest putting the code in another Codeunit, and keeping the subscriber Codeunit as small as possible.
This is even more important if the custom code only executes on a given condition.
An example of a Codeunit that you call from the subscriber Codeunit could be like this.
codeunit 50001 MyCodeCalledFromSubscriber
{
TableNo = "Sales Header";
trigger OnRun()
begin
ExecuteBusinessLogic(Rec);
end;
local procedure ExecuteBusinessLogic(SalesHeader: Record "Sales Header")
var
Customer: Record Customer;
begin
Message('I am pleased that you called.');
end;
}
And I change my subscriber Codeunit to only execute this code on a given condition.
codeunit 50100 MySubscriberCodeunit
{
SingleInstance = true;
trigger OnRun()
begin
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', true, true)]
local procedure MyProcedure(var SalesHeader: Record "Sales Header")
begin
ExecuteBusinessLogic(SalesHeader);
end;
local procedure ExecuteBusinessLogic(SalesHeader: Record "Sales Header")
var
MyCodeCalledFromSubscriber: Codeunit MyCodeCalledFromSubscriber;
begin
if SalesHeader."Document Type" = SalesHeader."Document Type"::Order then
MyCodeCalledFromSubscriber.Run(SalesHeader);
end;
}
This pattern makes sure that the execution is a fast as possible and no unneeded variables are populating the server memory.
On March 11th and 12th I will be teaching a VSCode and Modern NAV Development. This course will be held from 8:00am-5:00pm each day
The goal of the workshop is to learn about the new development tool for Business Central (Dynamics NAV), VSCode, GIT source control management and to experience what AL programming is about • What makes AL different from C/AL • How do you build and deploy a new BC feature • How can I convert my current code into AL • How to get ready for publishing your IP to AppSource • How to use GIT for you code
On the Developer track I will host three sessions.
Wednesday, March 13, 201910:15 AM – 11:45 AM, Room: Founders III
DEV75: How to Prepare Your Code for ALBCUG/NAVUG
Ready to completely re-think your all-in-one C/AL application? How about we try this: figure out how to split the code into “bricks” by functionality and/or processes, then turn that pile of bricks back into a usable solution. Can you migrate your customer data from the all-in-one C/AL database to the new continuous delivery cycle, replacing C/AL bricks with AL bricks. Let’s find out!
Wednesday, March 13, 20194:00 PM – 5:30 PM, Room: Founders II
DEV78: How I Got my Big Database Upgraded to Business Central
Your database upgrade takes longer than your available downtime window – bit of a problem, right? How about when executing all the upgrade processes on your database will take close to 10 days? Yeah, that’s a big problem. Of course you cannot stop a business for 10 days, but how do you shrink that to fit the 30-hour window over the weekend? You’ll hear the real life story and learn about the tools and methods you can use to streamline your upgrades.
Thursday, March 14, 20198:00 AM – 9:30 AM, Room: Founders III
DEV79: Breaking the Compilation Dependencies
Going to the extension model requires a simple structure to allow multiple extensions to talk to each other without having to put all of them into a compile dependency or into the same extension. Applying the standard API pattern inside the Business Central Service tier will give us the possibility to do all required functionality in a fast and easy way. This session is about explaining this pattern and giving some examples on how we have been using this pattern.
We have several ways of using the JSON interfaces. I will give few examples with the required C/AL code. I will be using Advania’s Online Banking solution interfaces for examples.
The Advania’s Online Banking solution is split into several different modules. The main module has the general framework. Then we have communication modules and functionality modules.
On/Off Question
A communication module should not work if the general framework does not exist or is not enabled for the current company. Hence, I need to ask the On/Off question
This is triggered by calling the solution enabled Codeunit.
IF NOT JsonInterfaceMgt.TryExecuteCodeunitIfExists('ADV Bank Services Enabled Mgt.','') THEN BEGIN
SetupNotification.MESSAGE := NotificationMsg;
SetupNotification.SEND;
END;
The interface function will search for the Codeunit, check for execution permissions and call the Codeunit with an empty request BLOB.
The “Enabled” Codeunit must respond with a “Success” variable of true or false.
[External] TryExecuteCodeunitIfExists(CodeunitName : Text;ErrorIfNotFound : Text) Success : Boolean
Object.SETRANGE(Type,Object.Type::Codeunit);
Object.SETRANGE(Name,CodeunitName);
IF NOT Object.FINDFIRST THEN
IF ErrorIfNotFound <> '' THEN
ERROR(ErrorIfNotFound)
ELSE
EXIT;
IF NOT HasCodeunitExecuteLicense(Object.ID,ErrorIfNotFound) THEN EXIT;
CODEUNIT.RUN(Object.ID,TempBlob);
InitializeFromTempBlob(TempBlob);
GetVariableBooleanValue(Success,'Success');
The “Enabled” Codeunit will test for Setup table read permission and if the “Enabled” flag has been set in the default record.
OnRun(VAR Rec : Record TempBlob)
TestEnabled(Rec);
LOCAL TestEnabled(VAR TempBlob : Record TempBlob)
WITH JsonInterfaceMgt DO BEGIN
Initialize;
AddVariable('Success',IsServiceEnabled);
GetAsTempBlob(TempBlob);
END;
IsServiceEnabled() : Boolean
IF NOT Setup.READPERMISSION THEN EXIT;
EXIT(Setup.GET AND Setup.Enabled);
This is how we can make sure that a module is installed and enabled before we start using it or any of the dependent modules.
Table Access Interface
The main module has a standard response table. We map some of the communication responses to this table via Data Exchange Definition. From other modules we like to be able to read the response from the response table.
The response table uses a GUID value for a primary key and has an integer field for the “Data Exchange Entry No.”. From the sub module we ask if a response exists for the current “Data Exchange Entry No.” by calling the interface.
FindResponse(DataExchEntryNo : Integer) Success : Boolean
WITH JsonInterfaceMgt DO BEGIN
Initialize;
AddVariable('DataExchEntryNo',DataExchEntryNo);
GetAsTempBlob(TempBlob);
ExecuteInterfaceCodeunitIfExists('ADV Bank Serv. Resp. Interface',TempBlob,ResponseInterfaceErr);
InitializeFromTempBlob(TempBlob);
GetVariableBooleanValue(Success,'Success');
END;
The Interface Codeunit for the response table will filter on the “Data Exchange Entry No.” and return the RecordID for that record if found.
OnRun(VAR Rec : Record TempBlob)
WITH JsonInterfaceMgt DO BEGIN
InitializeFromTempBlob(Rec);
GetVariableIntegerValue(DataExchEntryNo,'DataExchEntryNo');
Response.SETRANGE("Data Exch. Entry No.",DataExchEntryNo);
AddVariable('Success',Response.FINDFIRST);
IF Response.FINDFIRST THEN
AddRecordID(Response);
GetAsTempBlob(Rec);
END;
If the response is found we can ask for the value of any field from that record by calling
GetFieldValue(FieldName : Text) FieldValue : Text
WITH JsonInterfaceMgt DO
IF GetRecordByTableName('ADV Bank Service Response',RecRef) THEN
IF DataTypeMgt.FindFieldByName(RecRef,FldRef,FieldName) THEN
IF FORMAT(FldRef.TYPE) = 'BLOB' THEN BEGIN
TempBlob.Blob := FldRef.VALUE;
FieldValue := TempBlob.ReadAsTextWithCRLFLineSeparator();
END ELSE
FieldValue := FORMAT(FldRef.VALUE,0,9);
Processing Interface
Some processes can be both automatically and manually executed. For manual execution we like to display a request page on a Report. On that request page we can ask for variables, settings and verify before executing the process.
For automatic processing we have default settings and logic to find the correct variables before starting the process. And since one module should be able to start a process in the other then we use the JSON interface pattern for the processing Codeunit.
We also like to include the “Method” variable to add flexibility to the interface. Even if there is only one method in the current implementation.
OnRun(VAR Rec : Record TempBlob)
WITH JsonInterfaceMgt DO BEGIN
InitializeFromTempBlob(Rec);
IF NOT GetVariableTextValue(Method,'Method') OR (Method = '') THEN
ERROR(MethodNotFoundErr);
CASE Method OF
'BankAccountProcessing':
BankAccountProcessing(JsonInterfaceMgt);
END;
END;
LOCAL BankAccountProcessing(JsonInterfaceMgt : Codeunit "IS Json Interface Mgt.")
CheckSetup;
CompanyInformation.GET;
WITH JsonInterfaceMgt DO BEGIN
GetVariableTextValue(ClaimExportImportFormatCode, 'ClaimExportImportFormatCode');
GetVariableTextValue(BankAccountNo, 'BankAccountNo');
GetVariableDateValue(StartDate,'StartDate');
GetVariableDateValue(EndDate,'EndDate');
ValidateStartDate;
ValidateEndDate;
ValidateImportFormat;
BankAccount.SETRANGE("No.", BankAccountNo);
ClaimExportImportFormat.GET(ClaimExportImportFormatCode);
Initialize;
AddVariable('BankAccNo',BankAccountNo);
AddVariable('ClaimantID',CompanyInformation."Registration No.");
AddVariable('StartDate',StartDate);
AddVariable('EndDate',EndDate);
GetAsTempBlob(TempBlob);
Window.OPEN(ImportingFromBank);
IF BankAccount.FINDSET THEN REPEAT
DataExchDef.GET(ClaimExportImportFormat."Resp. Data Exch. Def. Code");
DataExch.INIT;
DataExch."Related Record" := BankAccount.RECORDID;
DataExch."Table Filters" := TempBlob.Blob;
DataExch."Data Exch. Def Code" := DataExchDef.Code;
DataExchLineDef.SETRANGE("Data Exch. Def Code",DataExchDef.Code);
DataExchLineDef.FINDFIRST;
DataExch."Data Exch. Line Def Code" := DataExchLineDef.Code;
DataExchDef.TESTFIELD("Ext. Data Handling Codeunit");
CODEUNIT.RUN(DataExchDef."Ext. Data Handling Codeunit",DataExch);
DataExch.INSERT;
IF DataExch.ImportToDataExch(DataExchDef) THEN BEGIN
DataExchMapping.GET(DataExchDef.Code,DataExchLineDef.Code,DATABASE::"ADV Claim Payment Batch Entry");
IF DataExchMapping."Pre-Mapping Codeunit" <> 0 THEN
CODEUNIT.RUN(DataExchMapping."Pre-Mapping Codeunit",DataExch);
DataExchMapping.TESTFIELD("Mapping Codeunit");
CODEUNIT.RUN(DataExchMapping."Mapping Codeunit",DataExch);
IF DataExchMapping."Post-Mapping Codeunit" <> 0 THEN
CODEUNIT.RUN(DataExchMapping."Post-Mapping Codeunit",DataExch);
END;
DataExch.DELETE(TRUE);
UNTIL BankAccount.NEXT = 0;
Window.CLOSE;
END;
Reading through the code above we can see that we are also using the JSON interface to pass settings to the Data Exchange Framework. We put the JSON configuration into the “Table Filters” BLOB field in the Data Exchange where we can use it later in the data processing.
From the Report we start the process using the JSON interface.
Bank Account - OnPreDataItem()
WITH JsonInterfaceMgt DO BEGIN
Initialize;
AddVariable('Method','BankAccountProcessing');
AddVariable('ClaimExportImportFormatCode', ClaimExportImportFormat.Code);
AddVariable('BankAccountNo', BankAccount."No.");
AddVariable('StartDate',StartDate);
AddVariable('EndDate',EndDate);
GetAsTempBlob(TempBlob);
ExecuteInterfaceCodeunitIfExists('ADV Import BCP Interface', TempBlob, '');
END;
The ExecuteInterfaceCodeunitIfExists will also verify that the Interface Codeunit exists and also verify the permissions before executing.
[External] ExecuteInterfaceCodeunitIfExists(CodeunitName : Text;VAR TempBlob : Record TempBlob;ErrorIfNotFound : Text)
Object.SETRANGE(Type,Object.Type::Codeunit);
Object.SETRANGE(Name,CodeunitName);
IF NOT Object.FINDFIRST THEN
IF ErrorIfNotFound <> '' THEN
ERROR(ErrorIfNotFound)
ELSE
EXIT;
IF NOT HasCodeunitExecuteLicense(Object.ID,ErrorIfNotFound) THEN EXIT;
CODEUNIT.RUN(Object.ID,TempBlob)
Extensible Interface
For some tasks it might be simple to have a single endpoint (Interface Codeunit) for multiple functionality. This can be achieved by combining Events and Interfaces.
We start by reading the required parameters from the JSON and then we raise an event for anyone to respond to the request.
OnRun(VAR Rec : Record TempBlob)
WITH JsonInterfaceMgt DO BEGIN
InitializeFromTempBlob(Rec);
IF NOT GetVariableTextValue(InterfaceType,'InterfaceType') THEN
ERROR(TypeErr);
IF NOT GetVariableTextValue(Method,'Method') THEN
ERROR(MethodErr);
OnInterfaceAccess(InterfaceType,Method,Rec);
END;
LOCAL [IntegrationEvent] OnInterfaceAccess(InterfaceType : Text;Method : Text;VAR TempBlob : Record TempBlob)
We can also pass the JSON Interface Codeunit, as that will contain the full JSON and will contain the full JSON for the response.
OnRun(VAR Rec : Record TempBlob)
WITH JsonInterfaceMgt DO BEGIN
InitializeFromTempBlob(Rec);
IF NOT GetVariableTextValue(InterfaceType,'InterfaceType') THEN
ERROR(TypeErr);
IF NOT GetVariableTextValue(Method,'Method') THEN
ERROR(MethodErr);
OnInterfaceAccess(InterfaceType,Method,JsonInterfaceMgt);
GetAsTempBlob(Rec);
END;
LOCAL [IntegrationEvent] OnInterfaceAccess(InterfaceType : Text;Method : Text;VAR JsonInterfaceMgt : Codeunit "IS Json Interface Mgt.")
One of the subscribers could look like this
LOCAL [EventSubscriber] OnInterfaceAccess(InterfaceType : Text;Method : Text;VAR JsonInterfaceMgt : Codeunit "IS Json Interface Mgt.")
IF InterfaceType = 'Claim' THEN
CASE Method OF
'Register':
Register(JsonInterfaceMgt);
'Edit':
Edit(JsonInterfaceMgt);
'AddExportImportFormat':
AddExportImportFormat(JsonInterfaceMgt);
'GetSetupCodeunitID':
GetSetupCodeunitID(JsonInterfaceMgt);
'GetDirection':
GetDirection(JsonInterfaceMgt);
'GetServiceUrl':
GetServiceUrl(JsonInterfaceMgt);
'GetExportImportFormat':
GetExportImportFormat(JsonInterfaceMgt);
'GetServiceMethod':
GetServiceMethod(JsonInterfaceMgt);
'ShowAndGetClaimFormat':
ShowAndGetClaimFormat(JsonInterfaceMgt);
'GetDataExchangeDefintionWithAction':
GetDataExchangeDefintionWithAction(JsonInterfaceMgt);
'GetOperationResultForClaimant':
GetOperationResultForClaimant(JsonInterfaceMgt);
'ShowClaimPayment':
ShowClaimPayment(JsonInterfaceMgt)
ELSE
ERROR(MethodErr,Method);
END;
Registration Interface
This pattern is similar to the discovery pattern, where an Event is raised to register possible modules into a temporary table. Example of that is the “OnRegisterServiceConnection” event in Table 1400, Service Connection.
Since we can’t have Event Subscriber in one module listening to an Event Publisher in another, without having compile dependencies, we have come up with a different solution.
We register functionality from the functionality module and the list of modules in stored in a database table. The table uses a GUID and the Language ID for a primary key, and then the view is filtered by the Language ID to only show one entry for each module.
This pattern gives me a list of possible modules for that given functionality. I can open the Setup Page for that module and I can execute the Interface Codeunit for that module as well. Both the Setup Page ID and the Interface Codeunit ID are object names.
The registration interface uses the Method variable to select the functionality. It can either register a new module or it can execute the method in the modules.
OnRun(VAR Rec : Record TempBlob)
WITH JsonInterfaceMgt DO BEGIN
InitializeFromTempBlob(Rec);
IF NOT GetVariableTextValue(Method,'Method') THEN
ERROR(MethodErr);
CASE Method OF
'Register':
RegisterCollectionApp(JsonInterfaceMgt);
ELSE
ExecuteMethodInApps(Rec);
END;
END;
LOCAL RegisterCollectionApp(JsonInterfaceMgt : Codeunit "IS Json Interface Mgt.")
WITH BankCollectionModule DO BEGIN
JsonInterfaceMgt.GetVariableGUIDValue(ID,'ID');
"Language ID" := GLOBALLANGUAGE();
IF FIND THEN EXIT;
INIT;
JsonInterfaceMgt.GetVariableTextValue(Name,'Name');
JsonInterfaceMgt.GetVariableTextValue("Setup Page ID",'SetupPageID');
JsonInterfaceMgt.GetVariableTextValue("Interface Codeunit ID",'InterfaceCodeunitID');
INSERT;
END;
[External] ExecuteMethodInApps(VAR TempBlob : Record TempBlob)
WITH BankCollectionModule DO BEGIN
SETCURRENTKEY("Interface Codeunit ID");
IF FINDSET THEN REPEAT
JsonInterfaceMgt.ExecuteInterfaceCodeunitIfExists("Interface Codeunit ID",TempBlob,'');
SETFILTER("Interface Codeunit ID",'>%1',"Interface Codeunit ID");
UNTIL NEXT = 0;
END;
In the “ExecuteMethodInApps” function I use the filters to make sure to only execute each Interface Codeunit once.
The registration is executed from the Setup & Configuration in the other module.
[External] RegisterCollectionApp()
WITH JsonInterfaceMgt DO BEGIN
Initialize();
AddVariable('Method','Register');
AddVariable('ID',GetCollectionAppID);
AddVariable('Name',ClaimAppName);
AddVariable('SetupPageID','ADV Claim Setup');
AddVariable('InterfaceCodeunitID','ADV Claim Interface Access');
GetAsTempBlob(TempBlob);
ExecuteInterfaceCodeunitIfExists('ADV Bank Collection App Access',TempBlob,'');
END;
Extend functionality using the Registered Modules.
As we have been taught we should open our functionality for other modules. This is done by adding Integration Events to our code.
LOCAL [IntegrationEvent] OnBeforePaymentPost(ClaimPaymentEntry : Record "ADV Claim Payment Batch Entry";VAR CustLedgEntry : Record "Cust. Ledger Entry";VAR UseClaimPaymentApplication : Boolean;VAR ToAccountType : 'G/L Account,Customer,Vendor,Bank Acco
LOCAL [IntegrationEvent] OnBeforePostGenJnlLine(VAR ClaimPaymentEntry : Record "ADV Claim Payment Batch Entry";VAR GenJournalLine : Record "Gen. Journal Line";VAR AppliedDocType : Option;VAR AppliedDocNo : Code[20];VAR AppliesToID : Code[50])
Where the Subscriber that needs to respond to this Publisher is in another module we need to extend the functionality using JSON interfaces.
First, we create a Codeunit within the Publisher module with Subscribers. The parameters in the Subscribers are converted to JSON and passed to the possible subscriber modules using the “ExecuteMethodInApps” function above.
LOCAL [EventSubscriber] OnBeforeClaimPaymentInsert(VAR ClaimPaymentEntry : Record "ADV Claim Payment Batch Entry")
GetClaimSettings(ClaimPaymentEntry);
LOCAL GetClaimSettings(VAR ClaimPaymentEntry : Record "ADV Claim Payment Batch Entry") Success : Boolean
JsonInterfaceMgt.Initialize;
JsonInterfaceMgt.AddVariable('Method','GetClaimSettings');
JsonInterfaceMgt.AddVariable('ClaimantID',ClaimPaymentEntry."Claimant Registration No.");
JsonInterfaceMgt.AddVariable('ClaimKey',ClaimPaymentEntry."Claim Account No.");
JsonInterfaceMgt.AddVariable('InterestDate',ClaimPaymentEntry."Interest Date");
JsonInterfaceMgt.GetAsTempBlob(TempBlob);
BankCollectionAppAccess.ExecuteMethodInApps(TempBlob);
JsonInterfaceMgt.InitializeFromTempBlob(TempBlob);
IF NOT JsonInterfaceMgt.GetVariableBooleanValue(Success,'Success') THEN EXIT;
ClaimPaymentEntry."Batch Code" := GetJsonProperty('BatchCode');
ClaimPaymentEntry."Template Code" := GetJsonProperty('TemplateCode');
ClaimPaymentEntry."Source Code" := GetJsonProperty('SourceCode');
ClaimPaymentEntry."Customer No." := GetJsonProperty('CustomerNo');
ClaimPaymentEntry."Customer Name" := GetJsonProperty('CustomerName');
The module that is extending this functionality will be able to answer to these request and supply the required response.
OnRun(VAR Rec : Record TempBlob)
IF NOT Setup.READPERMISSION THEN EXIT;
Setup.GET;
WITH JsonInterfaceMgt DO BEGIN
InitializeFromTempBlob(Rec);
IF NOT GetVariableTextValue(Method,'Method') THEN
ERROR(MethodErr);
CASE Method OF
'Register':
RegisterCollectionApp();
'GetByCustLedgEntryNo':
ReturnClaimForCustLedgEntryNo(Rec);
'GetCustLedgEntryLinkInfo':
ReturnClaimInfoForCustLedgEntryNo(Rec);
'DisplayCustLedgEntryLinkInfo':
DisplayClaimInfoForCustLedgEntryNo();
'GetClaimSettings':
ReturnClaimSettings(Rec);
'GetClaimTempateSettings':
ReturnClaimTemplateSettings(Rec);
'GetClaimPaymentApplicationID':
ReturnClaimPaymentApplicationID(Rec);
'AddToGenDataRequest':
ReturnGenDataRequest(Rec);
END;
END;
Azure Function
The last example we will show is the Azure Function. Some functionality requires execution in an Azure Function.
By making sure that our Azure Function understands the same JSON format used in our JSON Interface Codeunit we can easily prepare the request and read the response using the same methods.
We have the Azure Function Execution in that same JSON Codeunit. Hence, easily prepare the request and call the function in a similar way as for other interfaces.
JsonInterfaceMgt.Initialize;
JsonInterfaceMgt.AddVariable('Method',ServiceMethod);
JsonInterfaceMgt.AddVariable('Url',ServiceUrl);
JsonInterfaceMgt.AddVariable('Username',Username);
JsonInterfaceMgt.AddEncryptedVariable('Password',Password);
JsonInterfaceMgt.AddVariable('Certificate',CertificateValueAsBase64);
JsonInterfaceMgt.AddVariable('Xml',TempBlob.ReadAsTextWithCRLFLineSeparator);
Success := JsonInterfaceMgt.ExecuteAzureFunction;
IF JsonInterfaceMgt.GetVariableBLOBValue(TempBlob,'Xml') THEN
LogMgt.SetIncoming(TempBlob.ReadAsTextWithCRLFLineSeparator,'xml')
ELSE
LogMgt.SetIncoming(JsonInterfaceMgt.GetJSON,'json');
IF Success THEN
DataExch."File Content" := TempBlob.Blob;
The request JSON is posted to the Azure Function and the result read with a single function.
[External] ExecuteAzureFunction() Success : Boolean
GetAsTempBlob(TempBlob);
IF (NOT GetVariableTextValue(AzureServiceURL,'AzureServiceURL')) OR (AzureServiceURL = '') THEN
AzureServiceURL := 'https://<azurefunction>.azurewebsites.net/api/AzureProxy?code=<some access code>';
OnBeforeExecuteAzureFunction(TempBlob,AzureServiceURL,OmmitWebRequest);
IF NOT OmmitWebRequest THEN BEGIN
HttpWebRequestMgt.Initialize(AzureServiceURL);
HttpWebRequestMgt.DisableUI;
HttpWebRequestMgt.SetMethod('POST');
HttpWebRequestMgt.SetContentType('application/json');
HttpWebRequestMgt.SetReturnType('application/json');
HttpWebRequestMgt.AddBodyBlob(TempBlob);
TempBlob.INIT;
TempBlob.Blob.CREATEINSTREAM(ResponseInStream,TEXTENCODING::UTF8);
IF NOT HttpWebRequestMgt.GetResponse(ResponseInStream,HttpStatusCode,ResponseHeaders) THEN
IF NOT HttpWebRequestMgt.ProcessFaultResponse('http://www.advania.is') THEN BEGIN
Initialize;
AddVariable('Exception',GETLASTERRORTEXT);
EXIT(FALSE);
END;
END;
InitializeFromTempBlob(TempBlob);
GetVariableBooleanValue(Success,'Success');
We use the “OnBeforeExecuteAzureFunction” event with a manual binding for our Unit Tests.
In the Azure Function we read the request with standard JSON functions
Having standard ways of talking between modules and solutions has opened up for a lot of flexibility. We like to keep our solutions as small as possible.
We could mix “Methods” and “Versions” if we at later time need to be able to extend some of the interfaces. We need to honor the contract we have made for the interfaces. We must not make breaking changes to the interfaces, but we sure can extend them without any problems.
By attaching the JSON Interface Codeunit to the post I hope that you will use this pattern in your solutions. Use the Code freely. It is supplies as-is and without any responsibility, obligations or requirements.