Monday, May 12, 2025

Consuming External Web Services in D365FO


Enhancing Dynamics 365 Finance and Operations with Real-Time External Data

Integrating external web services into Dynamics 365 Finance and Operations (D365FO) can greatly enhance functionality, allowing for the use of real-time data from various sources. In this article, we will demonstrate how to consume an external web service in X++ using a practical example: retrieving currency exchange rates from a publicly available API.


💡 Use Case: Currency Conversion in the Sales Order Form

We aim to build a class that fetches the exchange rate between two currencies. The chosen API for this example is ExchangeRate-API, which provides conversion rates between multiple currencies as well as many other services.

Our specific implementation will:

  • Integrate into the Sales order form
  • Add a new button labeled "Show price in USD" (we assume that the company currency is USD)
  • Display both the conversion rate and the price in USD via infolog
  • Convert prices from Sales order currency to USD in real-time

Additionally, we'll leverage existing D365FO tables (Regulatory Service EMWebApplication and EMWebService) to securely store connection parameters and API keys. This approach promotes reuse and simplifies maintenance of external service configurations.

📌 Pro Tip: Although these tables are part of the Electronic Messaging (EM) functionality mainly intended for regulatory reporting, they can also be conveniently utilized for storing external web service configurations.


📋 Prerequisites

  • D365FO environment with development access
  • Access to a publicly available API (e.g., ExchangeRate-API)
  • API key for authentication: you can get it for free on the API web site

Implementation Guide

Step 1: Storing Connection Information

D365FO provides the EMWebApplication and EMWebService tables within the Regulatory Service module to manage external service connections. These tables allow you to store connection parameters securely.

  1. Add records for your application (e.g., 'wzhExchangeRateAPI')
  2. Create service records (e.g., 'wzhCurrencyPairConversion')
  3. Store the base URL and request method in these tables

Understanding the table structure:

  • Web Application table: Stores main connection information, such as base URL and general settings

  • Web Service table: Stores different API endpoints under the same application (like methods for Pair Conversion and Latest Rates)

Step 2: API response class wzhExchangeRateResponseDC

Create a new class named wzhExchangeRateResponseDC. This class contains methods to receive the API response.

// to be used as a response from wzhExchangeRateServiceConvert
[DataContract]
public class wzhExchangeRateResponseDC
{
    str         result;
    real        rate;

    [DataMember("result")]
    public str parmResult(str _result = result)
    {
        result = _result;
        return result;
    }

    [DataMember("conversion_rate")]
    public real parmRate(real _rate = rate)
    {
        rate = _rate;
        return rate;
    }

}

Step 3: Main logic class wzhExchangeRateService

Create a new class named wzhExchangeRateService. This class contains methods to initialize the service, retrieve conversion rates, and display the converted price. The class is designed to be triggered from a button click in the Sales order form. The construct method initializes the service object and sets the sales line parameter.

/// <summary>
/// Consumes external Web service for currency conversions
/// </summary>
class wzhExchangeRateService
{
    SalesLine salesLine;
    // to be run from a button
    static void main(Args _args)
    {
        SalesLine salesLine = _args.record();

        wzhExchangeRateService service = wzhExchangeRateService::construct(salesLine);
        service.run();
    }

    public SalesLine parmSalesLine(SalesLine _salesLine = salesLine)
    {
        salesLine = _salesLine;
        return salesLine;
    }

    static private wzhExchangeRateService construct(SalesLine _salesLine)
    {
        wzhExchangeRateService service = new wzhExchangeRateService();
        
        service.parmSalesLine(_salesLine);

        return  service;
    }

    public void run()
    {
        CurrencyCode currencyFrom   = salesline.CurrencyCode;
        CurrencyCode currencyTo     = Ledger::accountingCurrency();
        real rate = this.getConversionRate(currencyFrom, currencyTo);
        info(strFmt("Conversion rate from %1 to %2: %3", currencyFrom, currencyTo, rate));
        info(strFmt("Price in %1 is %2", currencyTo, salesLine.SalesPrice * rate));
    }

    /// <summary>
    /// Fetches the conversion rate between two given currencies.
    /// </summary>
    /// <param name="_currencyFrom">Base currency code (e.g., USD)</param>
    /// <param name="_currencyTo">Target currency code (e.g., EUR)</param>
    public real getConversionRate(CurrencyCode _currencyFrom, CurrencyCode _currencyTo)
    {
        // these constants can be saved in a separate parameters table
        const str                       appName     = 'wzhExchangeRateAPI';
        const str                       serviceId   = 'wzhCurrencyPairConversion';
        System.Net.HttpWebRequest       request;
        System.Net.HttpWebResponse      response;
        System.IO.StreamReader          reader;
        System.Net.WebHeaderCollection  httpHeader;
        str                             responseData, url, apiKey;
        EMWebApplication                webApp;
        EMWebService                    webService;
        container                       jsonContainer;
        Object                          jsonObject;
        Real                            conversionRate;
        System.String                   netString;
        System.Exception                netExcepn;
        str                             msg;
        str                             errMsg;
        wzhExchangeRateResponseDC       resp;

        try
        {
            // Step 1: Find the Web Application and Web Service records
            select firstonly webApp
                where webApp.Name == appName;

            if (!webApp.RecId)
            {
                throw error("Web application for currency conversions not found.");
            }

            select firstonly webService
                where webService.WebApplication == webApp.RecId
                   && webService.WebServiceId == serviceId;

            if (!webService.RecId)
            {
                throw error("Web Service for currency conversion rates not found.");
            }

            // Step 2: Retrieve the API key securely (from Azure Key Vault or encrypted table); check other methods getDigitalCertificate, getPrivateKey
            apiKey              = KeyVaultCertificateHelper::getManualSecretValue(webService.KeyVaultCertificateRef, false);
            apiKey              = '111914749114a4117ca989d2'; // for the test's sake

            // Step 3: Construct the API URL dynamically
            url                 = strFmt("%1/%2/%3/%4", webApp.BaseURL, webService.URL, _currencyFrom, _currencyTo);

            // Step 4: Create the HTTP request
            request             = System.Net.WebRequest::Create(url);
            request.Method      = webService.RequestMethod; // GET
            request.ContentType = webService.RequestContentType; // application/json
            httpHeader          = new System.Net.WebHeaderCollection();
            httpHeader.Add("Authorization", 'Bearer ' + apiKey); // security info
            request.set_Headers(httpHeader);

            // Step 5: Get the response
            response            = request.GetResponse();
            reader              = new System.IO.StreamReader(response.GetResponseStream());
            responseData        = reader.ReadToEnd();

            // Step 6: Parse the JSON response to a custom data contract bearing Success code and rate
            resp                = FormJsonSerializer::deserializeObject(classNum(wzhExchangeRateResponseDC), responseData);

            //if (resp && resp.parmResult() == webService.SuccessfulResponseCode) // this table success code is int and normally meant to be 200;
            if (resp && resp.parmResult() == webService.AdditionalSuccessfulResponseCodes) // but here we use this parameter as 'success'
            {
                conversionRate = resp.parmRate();
            }
            else
            {
                throw error("Error: Failed to parse the conversion rate.");
            }
        }
        catch (Exception::CLRError)
        {
            netExcepn   = CLRInterop::getLastException();

            if(netExcepn)
            {
                // something goes wrong with the service call
                // let's grab all details
                errMsg      = strFmt("Error: %1", netExcepn.InnerException.Message);
                msg         = strFmt("Failed to retrieve conversion rate %1->%2. \n%3", _currencyFrom, _currencyTo, errMsg);
            }
            throw error(strFmt("Error:%1", msg));
        }
        return conversionRate;
    }

}

Step 4: Fetch Conversion Rate

The core functionality lies in the getConversionRate method. It follows these steps:

  1. Finds the web application and service records using EMWebApplication and EMWebService tables
  2. Retrieves the API key securely, ideally using Azure Key Vault
  3. Constructs the URL dynamically based on input parameters
  4. Sends an HTTP request to the API and reads the response
  5. Parses the response using a custom data contract (wzhExchangeRateResponseDC)

🛡️ Handling Errors

The getConversionRate method includes robust error handling to manage connectivity issues or invalid API responses. Any errors are captured, and detailed messages are displayed to the user.


▶️ Running the Service

The run method prints the conversion rate and calculates the converted price based on the current sales line's currency and the accounting currency. This feature can be executed by clicking the "Show price in USD" button on the Sales order form.


🌟 Advantages of This Approach

  • Configuration-Driven: Uses Web Application and Web Service records instead of hardcoding URLs
  • Dynamic URL Construction: Retrieves settings directly from the D365FO database
  • Flexible: If the API URL changes, you only need to update the Web Application record
  • Efficient Management: The Web Application table holds the main connection information, while the Web Service table can manage multiple API endpoints under a single application

Final Thoughts

Consuming external web services in X++ allows D365FO to leverage real-time data from third-party sources. By using D365FO's existing EMWebApplication and EMWebService tables for storing connection data, we can enhance security and maintainability. Following this structured approach enables developers to integrate external services efficiently and securely.

Feel free to customize the class and extend its functionality according to your project requirements. Happy AXing!