The External Integration framework is an open-source solution designed for inbound and outbound integrations in D365FO.
In this blog post, I will describe how to implement a service endpoint using the External Integration framework.
X++ services are created using a standard service class and the Service and Service group objects.
Please note that a service is a synchronous integration pattern that increases system coupling. It should only be used when an asynchronous approach is not an option.
Before starting development, I analyzed several projects to see how different developers implemented their services. My key observations were:
Based on these observations, the following concept was implemented:
I have provided sample code in the following class.
To create a new service using the External Integration framework:
A code sample looks like this:
public class DEVIntegTutorialServiceTest extends DEVIntegServiceExportBase
{
/// DEMO #1 (DataTables / DataSet):
public DEVIntegServiceExportResponseContract getSOInfoDataTable(DEVIntegTutorialServiceTestContract _contract)
{
return this.serviceCallProcess(methodStr(DEVIntegTutorialServiceTest, calculateGetSOInfoDataTable), _contract);
}
public void calculateGetSOInfoDataTable(DEVIntegTutorialServiceTestContract _contract, DEVIntegServiceExportResponseContract _response)
{
this.validateAndDelay(_contract);
changecompany(_contract.parmCompanyId())
{
SalesTable salesTable = SalesTable::find(_contract.parmSalesId(), true);
if (!salesTable.RecId)
{
throw error(strFmt("Sales order %1 not found in company %2", _contract.parmSalesId(), _contract.parmCompanyId()));
}
this.initDataTableHelper();
dataTableHelper.addDataTable('SOHeader');
dataTableHelper.addRowItem('SalesId', salesTable.SalesId);
dataTableHelper.addRowItem('CustAccount', salesTable.CustAccount);
dataTableHelper.addRow();
// Lines table: multiple rows with sales lines
dataTableHelper.addDataTable('SOLine');
SalesLine salesLine;
while select salesLine
where salesLine.SalesId == salesTable.SalesId
{
dataTableHelper.addRowItem('SalesId', salesLine.SalesId);
dataTableHelper.addRowItem('ItemId', salesLine.ItemId);
dataTableHelper.addRowItem('SalesQty', salesLine.SalesQty);
dataTableHelper.addRow();
}
this.addDataTableHelperToResponse(_response);
}
}
/// DEMO #2 (Temp table / DataSet):
public DEVIntegServiceExportResponseContract getSOInfoTempTable(DEVIntegTutorialServiceTestContract _contract)
{
return this.serviceCallProcess(methodStr(DEVIntegTutorialServiceTest, calculateGetSOInfoTempTable), _contract);
}
public void calculateGetSOInfoTempTable(DEVIntegTutorialServiceTestContract _contract, DEVIntegServiceExportResponseContract _response)
{
this.validateAndDelay(_contract);
changecompany(_contract.parmCompanyId())
{
SalesTable salesTable = SalesTable::find(_contract.parmSalesId(), true);
if (!salesTable.RecId)
{
throw error(strFmt("Sales order %1 not found in company %2", _contract.parmSalesId(), _contract.parmCompanyId()));
}
SalesTable tmpSalesTable;
tmpSalesTable.setTmp();
tmpSalesTable.SalesId = salesTable.SalesId;
tmpSalesTable.CustAccount = salesTable.CustAccount;
tmpSalesTable.doinsert();
this.initDataTableHelper();
// Return temp table as a dataset DataTable (SOHeaderTmp)
dataTableHelper.addTmpTable('SOHeaderTmp', tmpSalesTable,
[
fieldNum(SalesTable, SalesId),
fieldNum(SalesTable, CustAccount)
]);
this.addDataTableHelperToResponse(_response);
}
}In case of errors, simply throw an exception, and the framework will handle it automatically. There are also a few helper methods available that can return either a temporary table or a dataset.
On the caller side, if the calling system is .NET-based, it needs to analyze the result by following these steps:
After creating a service class, it must be added to the External Integration > Setup > Service message type form.
From this form, we can also test the service class using the Test button.
The system will automatically create an empty contract. We just need to specify the parameter values and press the Execute button.
If the call is unsuccessful, an error message will be generated.
The framework provides different logging options tailored to various business use cases.
The following logging options are available:
Recommended values for production environments are "Request" or "Statistics & Errors".
Services can be consumed directly by connecting to the D365FO instance using an AppId. However, in recent projects, we have utilized the Azure API Management service. Adrià has written an excellent article detailing this approach.
In short, you will need to create an HTTP endpoint:
https://testsystemlink.sandbox.operations.dynamics.com/api/services/
With a new operation:
Display name: getSOInfoDataTable
Name: getsoinfodatatable
URL: /DEVIntegTutorialServiceTestGroup/DEVIntegTutorialServiceTest/getsoinfodatatableand apply the following policy
<policies>
<inbound>
<send-request mode="new" response-variable-name="bearerToken" timeout="20" ignore-error="false">
<set-url>https://login.microsoftonline.com/{{TenantId}}/oauth2/token</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/x-www-form-urlencoded</value>
</set-header>
<set-body>@{return "client_ID=4989d73b-1e25&client_secret=jSv8Q~m~jmO0QtZFmi_h4di&resource=https://testsystemlink.sandbox.operations.dynamics.com/&grant_type=client_credentials";}</set-body>
</send-request>
<set-backend-service base-url="https://testsystemlink.sandbox.operations.dynamics.com/api/services/DEVIntegTutorialServiceTestGroup" />
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (String)((IResponse)context.Variables["bearerToken"]).Body.As<JObject>()["access_token"])</value>
</set-header>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Next, test the API using the following parameters:
{"_contract": {"CompanyId":"USMF","DelaySeconds":1,"SalesId":"001036"}}Alternatively, if you are using Postman:
https://d365apiserv.azure-api.net/mytest/DEVIntegTutorialServiceTest/getSOInfoDataTable
POST
subscription-key
991874b1c..
Body raw
{
"_contract": {"CompanyId":"USMF","DelaySeconds":1,"SalesId":"001036"}
}
The External Integration framework offers the following benefits for service development:
All the classes discussed can be found on GitHub and serve as a great starting point for your own integrations.
I hope you found this post helpful. As always, if you have any suggestions for improvements or questions regarding this implementation, please don't hesitate to reach out.
Similar posts: