Canada
pawan.deep55@gmail.com

D365FO REST API Integration: Asynchronous Calls & Retry Guide

RESTAPID365FOxpp

D365FO REST API Integration: Asynchronous Calls & Retry Guide

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;
}
}


Author: Pawan Deep SinghI am a D365 Finance & Operations Technical Consultant. I have dedicated this blog to write about all D365FO , ax2012 x++ related tips and tricks I came across in my career. For any query feel free to contact me via the contact section.

 

No Comments

Add your comment