Building Your Own Export Framework with Dynamics AX (part 3)


Amazing how fast time flies between these posts! In the gap between part 2 and part 3 of this series, I was honored to receive the MVP Award in Dynamics AX.  I cannot fully express my gratitude to the Dynamics Community for such an award, and it is inspiring me to keep up the pace!

Today I’d like to wrap up this series on creating your own Export Framework.  In part 1, we created a simple WebAPI with Entity Framework that held a definition of “Customers” as an example.  In part 2, we created a Visual Studio solution integrated with AX to make use of some great tools such as the .NET WebClient and Dynamic ExpandoObjects. In part 3, we are going to polish things off in AX with a form, table, and simple hook to export all customers.  Let’s get started!

I always start from the ‘bottom->up’ so to speak, so a table will be our base. I’m calling this AxDataExport and it has a few generic fields such as APIObject, APIProperty, and some table fields:

This will hold our mapping settings for exports.  For a given Object, such as a Customer, we’ll want to map the export property with data from AX.   As we all know, AX is highly normalized and so it is likely that not all Customer data exists in a single table for your use.  That is why we also have a method field, so that we can reference a data method instead of a table field.  In this way, you can resolve most scenarios for data export.

Next, let’s build a Simple List form:

You’ll see we aren’t allowing new records to be created manually.  This is by design, as we want to enforce the integration somewhat.  The “Load” button calls out to the WebAPI and asks for a blank definition of all objects.  After a user clicks this button, any objects and properties which aren’t mapped are added to the table:

From here, the user can map the Customer properties of CustId, Name, and AxRecordId to data in AX:

In some cases, such as Customer name we know to exist in the DirPartyTable.  However, there is a convenient method on the CustTable called name() that returns this value for us:

The ObjectType is a special property that denotes the name of an API Object, so we do not map that to anything in AX. 

From here, we just need to build three classes for our exporting mechanism.  The first, is a data contract:

This accepts a few properties such as the record being exported, an optional string and a container for our record to cross tiers reliably.

Next, we’ll construct an event handler class with a single method called exportTriggered():

  1. public static void exportTriggered(XppPrePostArgs _args)
  2. {
  3. #define.APIObject('_apiObject')
  4. Common tableInstance;
  5. AxExportContract contract;
  6. SysOperationProgress progressBar;
  7. AxExportServiceBase exportService;
  8. str apiObjectName;
  9. #AviFiles
  10. ;
  11.  
  12. tableInstance = _args.getThis();
  13.  
  14. if (AxDataExport::isTableMapped(tableInstance.TableId))
  15. {
  16. progressBar = SysOperationProgress::newGeneral(#AviTransfer, 'Exporting...', 1);
  17. contract = AxExportContract::construct();
  18. contract.parmExportContainer(buf2Con(tableInstance));
  19.  
  20. if (_args.existsArg(#APIObject))
  21. {
  22. apiObjectName = _args.getArg(#APIObject);
  23. contract.parmAPIObject(apiObjectName);
  24. }
  25.  
  26. exportService = AxExportServiceBase::construct();
  27.  
  28. progressBar.incCount();
  29.  
  30. try
  31. {
  32. exportService.exportData(contract);
  33. }
  34. catch
  35. {
  36. checkFailed('Record was not exported, and cannot be updated');
  37. }
  38. }
  39.  
  40. }
  41.  

You’ll see this is double checking our table is mapped, and then prepares the export data contract for passing off to the export service. 

Finally, we build our export service class:

  1. [SysEntryPointAttribute(false)]
  2. public void exportData(AxExportContract _contract)
  3. {
  4. AXExportFramework.DataExportUtil util = new AXExportFramework.DataExportUtil();
  5. str baseURI = "";
  6. AxDataExport exportMap;
  7. Common tableInstance;
  8. System.String objectValue;
  9. str objVal, objectType;
  10. str 50 apiFilter;
  11. SysDictTable table;
  12. Types type;
  13. boolean useSysQuery = false;
  14. ExecutePermission perm;
  15. boolean exportSuccessful;
  16. date dateVal;
  17. ;
  18.  
  19. if(!baseURI)
  20. {
  21. return;
  22. }
  23. try
  24. {
  25. tableInstance = con2Buf(_contract.parmExportContainer());
  26. apiFilter = _contract.parmAPIObject() ? _contract.parmAPIObject() : SysQuery::valueLike('');
  27.  
  28. while select exportMap
  29. where exportMap.ExportTableId == tableInstance.TableId
  30. && exportMap.apiObject like apiFilter
  31. {
  32. useSysQuery = false;
  33.  
  34. table = new SysDictTable(tableInstance.TableId);
  35. if(objectType != exportMap.apiObject)
  36. {
  37. objectType = exportMap.apiObject;
  38. util.AddProperty(literalStr(ObjectType), objectType);
  39. }
  40. objVal = '';
  41. if(exportMap.ExportFieldId)
  42. {
  43. type = typeOf(tableInstance.getFieldValue(fieldId2name(exportMap.ExportTableId, exportMap.ExportFieldId)));
  44.  
  45. if(type == Types::String)
  46. {
  47. objVal = tableInstance.getFieldValue(fieldId2name(exportMap.ExportTableId, exportMap.ExportFieldId));
  48. }
  49. if(type == Types::UtcDateTime)
  50. {
  51. objVal = DateTimeUtil::toStr(DateTimeUtil::anyToDateTime(tableInstance.getFieldValue(fieldId2name(exportMap.ExportTableId, exportMap.ExportFieldId))));
  52. }
  53. if(type == Types::Date)
  54. {
  55. dateVal = tableInstance.getFieldValue(fieldId2name(exportMap.ExportTableId, exportMap.ExportFieldId));
  56. objVal = DateTimeUtil::toStr(DateTimeUtil::newDateTime(dateVal, 0));
  57. }
  58. useSysQuery = (type != Types::String && type != Types::UtcDateTime && type != Types::Date);
  59. if(useSysQuery)
  60. {
  61. objVal = SysQuery::value(tableInstance.getFieldValue(fieldId2name(exportMap.ExportTableId, exportMap.ExportFieldId)));
  62. if (objVal == SysQuery::valueEmptyString())
  63. {
  64. objVal = '';
  65. }
  66. }
  67.  
  68. objectValue = @objVal;
  69. }
  70. else
  71. {
  72. perm = new ExecutePermission();
  73. perm.assert();
  74. type = typeOf(table.callObject(exportMap.ExportTableMethod, tableInstance));
  75.  
  76. if(type == Types::String)
  77. {
  78. objVal = strFmt('%1', table.callObject(exportMap.ExportTableMethod, tableInstance));
  79. }
  80. else
  81. {
  82. objVal = SysQuery::value(table.callObject(exportMap.ExportTableMethod, tableInstance));
  83. if (objVal == SysQuery::valueEmptyString())
  84. {
  85. objVal = '';
  86. }
  87. strRem(objVal, '"');
  88. }
  89. objectValue = @objVal;
  90. CodeAccessPermission::revertAssert();
  91. }
  92. util.AddProperty(exportMap.apiProperty, objectValue);
  93.  
  94. }
  95.  
  96. exportSuccessful = util.Export(baseURI);
  97. if(!exportSuccessful)
  98. {
  99. throw Exception::Error;
  100. }
  101.  
  102. info('record exported');
  103. }
  104. catch
  105. {
  106. throw error('Export failed');
  107. }
  108.  
  109. }
  110.  


This method handles the conversion of the mapped table field or method to a String.  This is important, as we are working with XML serialization.  By passing all property values as Strings, we can dynamically populate a new or existing Customer.  It will be the API’s job to realize it has received data for a customer it already has in its system, and adjust accordingly.

Now this is great if we are running a job or something once a day or once every few hours. But, what if we want the updates real time? Just add a post-event handler to the insert/update methods of your mapped tables:

Now anytime it is updated either by a user or by another process, the changes will be exported to your integration API.  Of course, if you have processes that use other methods such as doInsert or doUpdate then you’ll want to monitor those as well. 

Also, if you implement a framework such as this after you already have thousands of Customers, just write a simple while-loop for the CustTable calling update() and you’ll dump out all of the records to your external system to get your baseline going.

Be looking for this base solution on CodePlex in the coming weeks! I’d like to clean it up a bit so that it is well commented, and I’d love to hear how you use it.