The steps required to set up an
exchange rate provider. For illustrative purposes, the OANDA exchange rate
service is used throughout this white paper. When you follow the steps
described in this paper, you will create a functional exchange rate provider. However,
that provider is not intended for use in production code. To request a free
OANDA test account and for information about the OANDA exchange rate service.
Exchange rate providers can be written in .NET or X++, based on the preference
and skills of each developer.
1. Import
currency exchange rates – The process that retrieves exchange
rates from exchange rate providers and imports them into the application. This
is a system operation that supports batch processing.
2. Exchange
rate provider – An X++ or .NET class that is responsible for retrieving exchange
rates from external sources.
3. Exchange
rate provider registration – The process of enabling an exchange
rate provider for use within the application. By default, exchange rate
providers are not registered when they are deployed.
4. Exchange
rate provider configuration – The configuration settings of an
exchange rate provider that determine how it will be used in the application.
5. Exchange
rate service – A free or paid subscription service that provides a list of
published exchange rates.
6. The framework –
The import currency exchange rates framework that coordinates the retrieval of
exchange rates from providers and the proper storage of the exchange rates
within Microsoft Dynamics AX.
Follow these steps to create an X++ exchange rate provider.
Create a new class that is derived from the ExchangeRateProvider
class.
class ExchangeRateProviderOanda
extends ExchangeRateProvider
{
}
Add the ExchangeRateProviderAttribute attribute to
enable the exchange rate provider framework to find the new provider during the
registration process.
[ExchangeRateProviderAttribute]
class ExchangeRateProviderOanda
extends ExchangeRateProvider
{
}
Add the ExchangeRateProviderIdAttribute
attribute to uniquely identify the provider. Use a string representation of
a GUID to ensure uniqueness. New GUIDs can be created on sites such as http://createguid.com. Also add the following
class-level variables, which are specific to this example and are not
necessarily required for every provider.
[ExchangeRateProviderAttribute,ExchangeRateProviderIdAttribute('61FDEDDB-B181-4C78-B35E-CBA193A70C75')]
public class
ExchangeRateProviderOanda extends
ExchangeRateProvider
{
str
serviceUrl;
str
serviceUrlForDateRange;
str
serviceClient;
int
serviceTimeout;
List rates;
List dates;
#define.ExchangeRatePosition(8)
#define.DatePosition(12)
#define.RatePrice("bid")
#define.RateDate("time")
#define.DateFormatMDY(213)
#define.ProviderId("FF7BF89C-18C1-42AF-91BA-07BBC3419D39")
#define.QuoteXPath("//response/quotes/quote")
#define.BidXPath("//bid")
#define.DateXPath("//quote/date")
#define.ServiceTimeout("serviceTimeout")
#define.ServiceURL("ServiceUrl")
#define.OANDADateFormat("yyyy-MM-dd")
#define.HttpWebRequestMethod("GET")
#define.HttpWebRequestContentType("application/xml")
#define.HttpHeaderAuthorization("Authorization")
#define.KeyTokenPrefix("Bearer ")
}
Implement the getName method.
The name used in the following code is hard-coded; however, in production code,
a label should be used to enable proper translation. The name provided here can
be changed by the user when that user sets up the provider’s configuration
information.
public ExchangeRateProviderName getName()
{
return
"Oanda exchange rate service New ";
}
Implement the getProviderId method. Return the same
GUID string that was used for the ExchangeRateProviderIdAttribute attribute.
public ExchangeRateProviderId getProviderId()
{
return
'61FDEDDB-B181-4C78-B35E-CBA193A70C75';
}
Implement
the getSupportedOptions method.
1.
Set the parmDoesSupportSpecificCurrencyPairs option to true only
if the exchange rate service requires that a source and destination currency be
passed to get an exchange rate. Many exchange rate services return rates with
respect to a fixed currency or a given set of currency pairs. For these
services, the value of this option should be set to false.
2.
Set the parmDoesSupportSpecificDates option to true if the
exchange rate service enables rates to be requested for specific dates.
Services that provide the exchange rate only for the current date will set this
value to false.
3. Set the parmFixedBaseIsoCurrency option to the
three-character ISO currency code that represents the fixed base currency of
the exchange rates being returned from the exchange rate service. If the
exchange rate service does not support a fixed base currency, return an empty
string. For example, the euro is often used as a fixed base currency. When you
create a new provider, be sure to research the exchange rate service to select
the correct value.
public ExchangeRateProviderSupportedOptions
getSupportedOptions()
{
ExchangeRateProviderSupportedOptions supportedOptions =
ExchangeRateProviderSupportedOptions::construct();
supportedOptions.parmDoesSupportSpecificCurrencyPairs(false);
supportedOptions.parmDoesSupportSpecificDates(false);
supportedOptions.parmFixedBaseIsoCurrency('');
return
supportedOptions;
}
Implement the getConfigurationDefaults method.
Configuration defaults are name-value pairs that represent the default
configuration settings for the exchange rate provider. These settings will be
automatically loaded for use when the provider is registered, but they can be
changed by the user. Take the necessary precautions when converting these
strings into usable values.
public ExchangeRateProviderConfigDefaults getConfigurationDefaults()
{
ExchangeRateProviderConfigDefaults configurationDefaults =
ExchangeRateProviderConfigDefaults::construct();
configurationDefaults.addNameValueConfigurationPair('ServiceTimeout', '5000');
configurationDefaults.addNameValueConfigurationPair('ServiceURL', 'https://www.oanda.com/rates/api/v1/rates/%1.xml?
decimal_places=5&date=%2&fields=averages"e=%3');
configurationDefaults.addNameValueConfigurationPair('serviceClient', 'oandatest');
return
configurationDefaults;
}
Implement the getExchangeRates method. This method
uses the configuration information and the provided ExchangeRateRequest class
instance to call out to the exchange rate source and return the appropriate ExchangeRateResponse
class instance. When writing this method, consider these important points:
1. Any of the configuration information that is required should be
retrieved from the ExchangeRateProviderConfig class. Calling the getPropertyValue
method on that class will provide the string representation of the property
value for the property key provided. Take the necessary precautions when
converting this string value to another type.
2. Some providers require explicit currency pairs when requesting
exchange rates. These are the same providers that would set the ExchangeRateProviderSupportedOptions.parmDoesSupportSpecificCurrencyPairs
property to true. When this is the case, it is necessary to use the
currency pairs provided by the ExchangeRateRequest class instance to
drive the retrieval process. The OANDA provider implementation that follows
shows a good example of this.
3. Other providers that do not support specific currency pairs
normally return data for a fixed set of currency pairs. When this is the case,
the currency pairs that are provided by the ExchangeRateRequest class
instance can be ignored. Providers should return all the rates that are
available, and the framework will import the correct ones, based on the user’s
decision to automatically create the necessary currency pairs or not. The
LithuanianBankProvider provider and CentralBankOfEuropeProvider provider that
ship with Microsoft Dynamics AX are good examples of this.
4. The ExchangeRateRequest class instance has a property
called parmImportDateType that indicates what dates should be used to
retrieve exchange rates from the service. The two options are CurrentDate and
DateRange.
5. CurrentDate is meant to retrieve the most up-to-date exchange rate from the
exchange rate service. When this is passed to the provider, the framework will
also set ExchangeRateRequest.parmFromDate and parmToDate to the
system date of the Application Object Server (AOS) computer that is making the
request. If exchange rate services support the retrieval of exchange rates for
specific dates, the date provided by the framework should be passed. If the
exchange rate service instead provides a call to get the most current exchange
rate (whatever the date may be), it is necessary to check the date returned to
ensure that it is less than or equal to the requested date.
6. DateRange is meant to retrieve a specific date range of exchange rates. In
this case, only exchange rates within that date range should be allowed. If an
exchange rate service requires that specific dates be included in the request,
this is straightforward. If an exchange rate service instead returns a group of
historical dates that may be outside the valid range of dates, it is up to the
provider to filter out the dates that are not pertinent before passing them
back to the framework.
7. When returning exchange rates, always use the date provided by the
exchange rate service rather than the dates supplied by the ExchangeRateRequest
class instance. This ensures that the exchange rate returned is associated
with the correct date, because there are scenarios where an exchange rate
service can return rates for dates that were not expected. For example, if an
exchange rate is requested for a date in the future, some providers return the
most recent exchange rate instead of throwing an error or returning nothing.
8. If errors are encountered when you attempt to retrieve exchange
rates from the exchange rate service, do not throw custom error messages. The
framework will alert the user that something is wrong by throwing generic error
messages stating that the expected currency pairs could not be retrieved from
the provider. If it is necessary to log additional errors, use the event log
instead.
public ExchangeRateResponse
getExchangeRates(ExchangeRateRequest _exchangeRateRequest)
{
ExchangeRateResponse
response = ExchangeRateResponse::construct();
ExchangeRateResponseCurrencyPair currencyPairResponse;
ExchangeRateResponseExchangeRate exchangeRateResponse;
ExchangeRateRequestCurrencyPair currencyPairRequest;
ExchangeRateProviderConfig
config = ExchangeRateProviderConfig::construct();
str
oandaRequestString;
date
currentDate;
CurrencyExchangeRate
exchangeRate;
System.Net.WebResponse
webResponse;
System.IO.StreamReader
streamReader;
System.IO.Stream stream;
System.Net.HttpWebRequest
httpWebRequest;
ListEnumerator
rateEnumerator, dateEnumerator;
System.Net.WebHeaderCollection webCollection;
System.DateTime fromDate,
fromUTCDate;
System.TimeZone
localTimeZone;
int
compareResult;
str
XMLOut;
str
dateForRequest;
rates = new List(Types::Real);
dates = new List(Types::Date);
localTimeZone =
System.TimeZone::get_CurrentTimeZone();
//
Iterate over the requested currency pairs. This is only required for providers
//
that support specific currency pairs.
_exchangeRateRequest.initializeCurrencyPairEnumerator();
while(_exchangeRateRequest.moveNextCurrencyPair())
{
serviceTimeout = str2int(config.getPropertyValue(this.getProviderId(),
#ServiceTimeout));
serviceUrl =
config.getPropertyValue(this.getProviderId(), #ServiceURL);
// Process each date separately.
// If we attempt to make a single request for multiple
dates
// then OANDA will average the rates across the dates which
is not what we want.
fromDate =
_exchangeRateRequest.parmFromDate();
compareResult =
fromDate.CompareTo(_exchangeRateRequest.parmToDate());
while (compareResult <= 0)
{
currencyPairRequest
= _exchangeRateRequest.getCurrentCurrencyPair();
currencyPairResponse = ExchangeRateResponseCurrencyPair::construct();
currencyPairResponse.parmFromCurrency(currencyPairRequest.parmFromCurrency());
currencyPairResponse.parmToCurrency(currencyPairRequest.parmToCurrency());
// All rates are requested with a display factor of 1 for
this provider. If the
// rates internally are represented using a different
exchange rate display to UTC which is required by OANDA
fromUTCDate =
localTimeZone.ToUniversalTime(fromDate);
dateForRequest =
fromUTCDate.ToString(#OANDADateFormat);
// Build the request URL.
oandaRequestString
= strFmt(serviceUrl,
currencyPairRequest.parmFromCurrency(),
currencyPairRequest.parmToCurrency(),
dateForRequest,
dateForRequest);
// Configure the request for OANDA.
httpWebRequest =
System.Net.WebRequest::CreateHttp(oandaRequestString);
httpWebRequest.set_Method(#HttpWebRequestMethod);
httpWebRequest.set_ContentType(#HttpWebRequestContentType);
httpWebRequest.set_Timeout(serviceTimeout);
// Authentication
webCollection =
httpWebRequest.get_Headers();
webCollection.Add(#HttpHeaderAuthorization, #KeyTokenPrefix +
//TODO: Retrieve and concatenate your Key provided by OANDA);
try
{
// Invoke the service
webResponse =
httpWebRequest.GetResponse();
// Retrieve the XML response.
stream =
webResponse.GetResponseStream();
streamReader = new System.IO.StreamReader(stream);
XMLOut =
streamReader.ReadToEnd();
// Parse the XML to retrieve the rate and date.
this.readRate(XMLOut);
rateEnumerator
= rates.getEnumerator();
dateEnumerator
= dates.getEnumerator();
// Create the Exchange Rate Provider Response.
dateEnumerator.moveNext();
exchangeRate =
rateEnumerator.current();
currentDate =
dateEnumerator.current();
if
(currentDate != dateNull() && exchangeRate)
{
exchangeRateResponse = ExchangeRateResponseExchangeRate::construct();
exchangeRateResponse.parmValidFrom(currentDate);
exchangeRateResponse.parmExchangeRate(exchangeRate);
currencyPairResponse.addExchangeRate(exchangeRateResponse);
currentDate
= dateNull();
exchangeRate = 0;
}
}
catch
(Exception::CLRError)
{
// The service call did not complete. Swallow the exception
and try the next
// currency pair. The framework will be able to determine
which currency
// pairs were successfully retrieved and will display the
appropriate
// warnings to the user.
}
response.addOrUpdateCurrencyPair(currencyPairResponse);
rates = new List(Types::Real);
dates = new List(Types::Date);
fromDate =
fromDate.AddDays(1);
compareResult =
fromDate.CompareTo(_exchangeRateRequest.parmToDate());
}
}
return
response;
}
Implement the following helper methods. These are specific
to this example and are not required for every provider.
public void
readRate(str _xmlString)
{
System.Xml.XmlDocument
xmlDom = new
System.Xml.XmlDocument();
System.Xml.XmlNode
xmlQuoteNode, xmlBidNode, xmlDateNode;
CurrencyExchangeRate
exchangeRate;
ValidFromDate exchangeDate;
str
value;
xmlDom.LoadXml(_xmlString);
//
Find the Quote
xmlQuoteNode =
xmlDom.SelectSingleNode(#QuoteXPath);
if
(xmlQuoteNode)
{
//
Find the exchange rate
xmlBidNode =
xmlQuoteNode.SelectSingleNode(#BidXPath);
if (xmlBidNode)
{
value =
xmlBidNode.get_InnerText();
exchangeRate = str2num(value);
if (exchangeRate)
{
rates.addEnd(exchangeRate);
}
}
//Find
the date of the exchange rate.
xmlDateNode =
xmlQuoteNode.SelectSingleNode(#DateXPath);
if (xmlDateNode)
{
value =
xmlDateNode.get_InnerText();
// convert the date from UTC to local timezone.
exchangeDate =
System.DateTime::Parse(value,
System.Globalization.CultureInfo::get_CurrentUICulture(),
System.Globalization.DateTimeStyles::AssumeUniversal);
if (exchangeRate)
{
dates.addEnd(exchangeDate);
}
}
}
}
Compile the ExchangeRateProviderOanda class, and
then generate incremental CIL. The provider will be run as part of a
SysOperation.
2 comments
commentsGreat Article.. Thank you for sharing..
ReplyD365 Finance and Operations Training
D365 Finance and Operations Online Training
D365 Finance Training
D365 Operations Training
Great Article.. Thank you for sharing..
ReplyD365 Finance and Operations Training
D365 Finance and Operations Online Training
D365 Finance Training
D365 Operations Training
Thanks for comments.....