
D365FO REST API Integration: Asynchronous Calls & Retry Guide
D365FO REST API integration can be challenging in Dynamics 365 Finance and Operations, especially when handling asynchronous requests with retry logic for reliability under high demand. During peak times, your D365FO instance may face network glitches, server timeouts, or rate limits from the APIs you’re integrating with. This guide walks you through creating a robust asynchronous API client in D365FO that uses exponential backoff and custom error handling. This approach minimizes disruptions and maintains a steady data flow, even when APIs encounter temporary failures.
The Approach: An Asynchronous API Client with Exponential Backoff
This client tackles those typical issues head-on with:
- Asynchronous Calls: By handling API requests asynchronously, it doesn’t hold up the main process, so everything else keeps moving smoothly.
- Exponential Backoff Retry Mechanism: Each retry happens with a progressively longer delay, minimizing repeated requests too quickly.
- Error Handling: It catches and logs errors, tracks retries, and provides clear messages to make debugging easier.
This code uses D365FO’s SysHttpCommunicationClient
for API communication and a custom retry function to handle intermittent issues. Below is the code and an explanation of how each part works.
APIClient.XPP
using System.Net; | |
using System.Net.Http; | |
using System.Threading; | |
using Microsoft.Dynamics.ServiceFramework.Communication.Http; | |
/// <summary> | |
/// Client class for handling asynchronous API calls with retry logic and error handling in D365FO. | |
/// </summary> | |
internal final class ApiClient | |
{ | |
#TimeConstants | |
private str apiUrl; // API endpoint URL | |
private str authToken; // Authorization token for API access | |
private int maxRetryCount; // Maximum number of retry attempts | |
private System.TimeSpan requestTimeout; // Request timeout duration | |
private SysHttpCommunicationClient httpClient; // HTTP client for sending requests | |
System.Net.HttpStatusCode responseStatusCode; // Holds response status code | |
private const int MaxExponentialBackoffDelayMs = 1 * #millisecondsPerMinute; | |
/// <summary> | |
/// Constructor to initialize the API client with necessary configurations. | |
/// </summary> | |
public void new( | |
str _apiUrl = "", // API URL | |
str _authToken = "", // Authorization token | |
int _maxRetryCount = 5, // Maximum retry attempts (default 5) | |
System.TimeSpan _requestTimeout = new System.TimeSpan(0, 1, 0)) // Default timeout: 1 minute | |
{ | |
// Initialize API and retry configurations | |
apiUrl = _apiUrl; | |
authToken = _authToken; | |
maxRetryCount = _maxRetryCount > 0 ? _maxRetryCount : 5; | |
requestTimeout = _requestTimeout; | |
// Configure the HTTP client | |
SysHttpCommunicationClientFactory factory = SysHttpCommunicationClientFactory::construct(); | |
SysHttpCommunicationClientBuilder clientBuilder = SysHttpCommunicationClientBuilder::construct(factory); | |
SysHttpClientOptions httpClientOptions = SysHttpClientOptions::construct(); | |
httpClientOptions.parmTimeout(requestTimeout); | |
// Enable monitoring and correlation for better tracking | |
clientBuilder.setIsCorrelated(true).setIsMonitored(true).setOptions(httpClientOptions); | |
httpClient = clientBuilder.createClient(); | |
} | |
/// <summary> | |
/// Asynchronously sends an API call with retry logic to manage transient errors. | |
/// </summary> | |
public SysHttpResponseMessageTask postApiCallWithRetryAsync(RequestPayload payload) | |
{ | |
HttpRequestMessage request = this.createHttpRequestMessage("POST", apiUrl, payload, authToken); | |
int retryCount = 0; | |
SysHttpResponseMessageTask requestResponseMessageTask; | |
try | |
{ | |
// Attempt to send the request asynchronously | |
requestResponseMessageTask = httpClient.sendAsync(request, System.Threading.CancellationToken::None); | |
} | |
catch (Exception::CLRError) | |
{ | |
// Handle CLR errors and retry if necessary | |
handleClrErrorWithRetry(request, payload, retryCount); | |
} | |
return requestResponseMessageTask; | |
} | |
/// <summary> | |
/// Handles CLR errors with retry logic, applying exponential backoff delay, logging attempts, and extracting web exceptions. | |
/// </summary> | |
private void handleClrErrorWithRetry(HttpRequestMessage request, RequestPayload payload, int retryCount) | |
{ | |
var lastCLRException = CLRInterop::getLastException(); | |
var webException = ApiClient::extractWebExceptionFromLastCLRException(lastCLRException); | |
if (webException) | |
{ | |
// If there’s a WebException, handle and log it | |
using (var response = webException.Response) | |
{ | |
if (response) | |
{ | |
responseStatusCode = (webException.Response as System.Net.HttpWebResponse).StatusCode; | |
this.responseWarning(funcName(), ApiClient::readResponse(response), responseStatusCode); | |
} | |
} | |
} | |
else | |
{ | |
// If not a WebException, handle as a generic non-success response | |
NonSuccessHttpResponseException nonSuccessException = ApiClient::extractNonSuccessHttpResponseExceptionFromLastCLRException(lastCLRException); | |
if (nonSuccessException) | |
{ | |
responseStatusCode = nonSuccessException.RemoteStatusCode; | |
this.responseWarning(funcName(), nonSuccessException.Message, responseStatusCode); | |
} | |
} | |
if (retryCount <= maxRetryCount) | |
{ | |
// Calculate exponential backoff delay and increment retry count | |
var delayTime = this.calculateExponentialBackoffDelay(retryCount); | |
retryCount++; | |
// Log retry attempt with delay details | |
this.logRetryToDatabase("postApiCallWithRetryAsync", | |
strFmt("Retry DelayTime:%1 Method:%2 URL:%3", delayTime, request.Method.ToString(), request.RequestUri.AbsoluteUri), | |
retryCount); | |
// Wait for delay and retry | |
sleep(delayTime); | |
retry; | |
} | |
else | |
{ | |
throw error("Asynchronous retry count exceeded"); | |
} | |
} | |
/// <summary> | |
/// Calculates delay for exponential backoff, increasing delay with each retry. | |
/// </summary> | |
private int calculateExponentialBackoffDelay(int retryCount) | |
{ | |
// Exponential delay calculation: 2^retryCount seconds | |
var delayTimeMS = power(2, retryCount) * #millisecondsPerSecond; | |
return min(MaxExponentialBackoffDelayMs, delayTimeMS); // Cap delay to prevent excessive wait times | |
} | |
/// <summary> | |
/// Creates the HTTP request message with headers and payload. | |
/// </summary> | |
private HttpRequestMessage createHttpRequestMessage(str _httpMethod, str _apiUrl, Object _requestContract, str _authToken) | |
{ | |
HttpMethod httpMethod = new HttpMethod(_httpMethod); | |
HttpRequestMessage request = new HttpRequestMessage(httpMethod, new System.Uri(_apiUrl)); | |
request.Headers.Add("Authorization", "Bearer " + _authToken); | |
// Add JSON payload if POST or PUT request | |
if (_httpMethod == "POST" || _httpMethod == "PUT") | |
{ | |
str json = FormJSONSerializer::serializeClass(_requestContract); | |
request.Content = new StringContent(json, System.Text.Encoding::UTF8, "application/json"); | |
} | |
return request; // Return fully prepared request | |
} | |
/// <summary> | |
/// Logs each retry attempt to the database for tracking purposes. | |
/// </summary> | |
private void logRetryToDatabase(str _funcName, str _logMessage, int _retryCount) | |
{ | |
// Log retry attempt (e.g., function name, message, retry count) for auditing and debugging | |
// Code for database logging here | |
} | |
/// <summary> | |
/// Extracts the web exception from last CLR exception. | |
/// </summary> | |
protected static System.Net.WebException extractWebExceptionFromLastCLRException(CLRObject _lastCLRException) | |
{ | |
var exception = _lastCLRException; | |
while (exception) | |
{ | |
var webException = exception as System.Net.WebException; | |
if (webException) | |
{ | |
return webException; | |
} | |
exception = exception.get_InnerException(); | |
} | |
return null; | |
} | |
/// <summary> | |
/// Extracts non-success HTTP response exception from the last CLR exception. | |
/// </summary> | |
protected static NonSuccessHttpResponseException extractNonSuccessHttpResponseExceptionFromLastCLRException(CLRObject _lastCLRException) | |
{ | |
var exception = _lastCLRException; | |
while (exception) | |
{ | |
var nonSuccessException = exception as NonSuccessHttpResponseException; | |
if (nonSuccessException) | |
{ | |
return nonSuccessException; | |
} | |
exception = exception.get_InnerException(); | |
} | |
return null; | |
} | |
/// <summary> | |
/// Reads and returns the response content from an HTTP web response. | |
/// </summary> | |
internal static str readResponse(System.Net.HttpWebResponse _httpResponse) | |
{ | |
using (var streamReader = new System.IO.StreamReader(_httpResponse.GetResponseStream())) | |
{ | |
return streamReader.ReadToEnd(); | |
} | |
} | |
/// <summary> | |
/// Logs response warnings for unsuccessful requests. | |
/// </summary> | |
protected void responseWarning(str _funcName, str _responseStreamContent, int _statusCode=0) | |
{ | |
str warningMsg; | |
if (_responseStreamContent) | |
{ | |
warningMsg = strFmt("Web Exception Response content: %1 Status code: %2", _responseStreamContent, _statusCode); | |
} | |
else | |
{ | |
warningMsg = "Web Exception Response is empty"; | |
} | |
warning(warningMsg); | |
this.logEventDetail(_funcName, warningMsg, _statusCode); | |
} | |
/// <summary> | |
/// Logs detailed event information, useful for tracking retries and warnings. | |
/// </summary> | |
private void logEventDetail(str _funcName, str _logMessage, int _statusCode=0, int _retryCount=0) | |
{ | |
APIEventLog log; | |
ttsbegin; | |
log.LogFunctionName = _funcName; | |
log.LogMessage = _logMessage; | |
log.ResponseStatusCode = _statusCode; | |
log.RetryCount = _retryCount; | |
log.insert(); | |
ttscommit; | |
} | |
} |
APICaller.XPP
/// <summary> | |
/// Caller class for invoking API requests using the ApiClient with retry and error handling. | |
/// </summary> | |
internal class ApiCaller | |
{ | |
private ApiClient apiClient; | |
/// <summary> | |
/// Initializes the caller class with necessary configurations for the API client. | |
/// </summary> | |
public ApiCaller() | |
{ | |
// Set up API endpoint and authorization token for the ApiClient instance | |
str apiUrl = "https://axparadise.com/data"; | |
str authToken = "yourAuthToken"; // Replace with your actual auth token | |
int maxRetryCount = 5; | |
System.TimeSpan requestTimeout = new System.TimeSpan(0, 1, 0); // 1-minute timeout | |
// Initialize the ApiClient instance with configurations | |
apiClient = new ApiClient(apiUrl, authToken, maxRetryCount, requestTimeout); | |
} | |
/// <summary> | |
/// Method to execute the API call and handle response or errors. | |
/// </summary> | |
public void ExecuteApiCall() | |
{ | |
RequestPayload payload = this.createPayload(); // Method to create request payload | |
try | |
{ | |
// Call ApiClient’s method to send an asynchronous API request with retry logic | |
SysHttpResponseMessageTask responseTask = apiClient.postApiCallWithRetryAsync(payload); | |
//below code is optional and can be removed if we don't want to wait for API response | |
// for example - calling a webhook which responds with HTTP 204 may not be required here to handle the response | |
// Wait on a response from the service | |
var requestResponseTask = responseTask.getTask(); | |
requestResponseTask.Wait(); | |
// Once the service has responded, use reflection to get the result of the service's response | |
// and return that result to the service client making this request | |
var type = requestResponseTask.GetType(); | |
var resultProperty = type.GetProperty(ResponseResultFieldName); | |
HttpResponseMessage response = resultProperty.GetValue(requestResponseTask); | |
if (response && response.isSuccessStatusCode()) | |
{ | |
// Handle successful response | |
str responseContent = response.contentAsString(); | |
info(strFmt("API Response: %1", responseContent)); | |
} | |
else | |
{ | |
// Handle unsuccessful response or null response | |
warning("API call did not return a successful response."); | |
} | |
} | |
catch (Exception::Error ex) | |
{ | |
// Handle exceptions and log details if retries are exhausted or a fatal error occurs | |
error(strFmt("API call failed after retries. Error: %1", ex.message())); | |
} | |
} | |
/// <summary> | |
/// Creates a sample payload for the API request. | |
/// </summary> | |
private RequestPayload createPayload() | |
{ | |
RequestPayload payload = new RequestPayload(); //DataContract class | |
// Populate the payload properties as per API requirements | |
payload.field1 = "value1"; | |
payload.field2 = "value2"; | |
// Add additional fields as necessary | |
return payload; | |
} | |
} | |
[DataContract] | |
internal final class RequestPayload | |
{ | |
private str field1, field2; | |
[DataMember('field1')] | |
public str parmField1(str _field1 = field1) { | |
field1 = _field1; | |
return field1; | |
} | |
[DataMember('field2')] | |
public str parmField2(str _field2 = field2) { | |
field2 = _field2; | |
return field2; | |
} | |
} |
I hope this article helped you implement asynchronous REST API integration with HTTP requests and retry logic in D365FO (X++) for Dynamics 365 Finance and Operations. Don’t forget to share this article if you found it useful! You may also want to check out our guide on converting strings to enums with
Convert string into enum str2enum in x++.
No Comments