Microsoft Dynamics AX 2012 – File Exchange (Export) using Web Services (WS)
Purpose: The purpose of this document to illustrate how to implement integration with Microsoft Dynamics AX 2012 based on File Exchange.
Challenge: In certain scenarios you need to implement integration with Microsoft Dynamics AX 2012 by means of File Exchange which is dictated by the software you integrate to or other architectural considerations. For the purposes of integration with Microsoft Dynamics AX 2012 based on File Exchange you can use Web Services approach.
Solution: The recommended way to integrate with Microsoft Dynamics AX 2012 is by using Web Services. As we know Microsoft Dynamics AX 2012 supports AIF Document Web Services and WCF Custom Web Services. In this article I'll implement integration with Microsoft Dynamics AX 2012 based on File Exchange using AIF Document Web Services and WCF Custom Web Services. My goal for this integration will be to organize continuous data export (CSV data feed) from Microsoft Dynamics AX 2012 using WCF Custom Web Services. I will also pay close attention to how to use built-in capabilities of AIF Outbound ports and File system adapter, so I can spend the most of the time focusing on business scenario itself (for example, what data I want to export to a file) and not on technical aspects (for example, how to export data into a file)
Walkthrough
For the purpose of this walkthrough I'd want to use WCF Custom Web Services which allow me to write a minimum code and provide a great flexibility when implementing data export scenario. Please also note that in this scenario as the result I want to get a CSV (Comma Separated Value) file.
In my scenario I'll be exporting a custom data based on a brand-new data model I've introduced
In order to implement WCF Custom Web Services I'll have to develop Service Contract and Data Contacts classes. To facilitate this effort I created 2 simple jobs which will help me to generate a backbone of Service Contract and Data Contacts classes based on existing data model
Please see how I implemented ServiceContract and DataContract jobs below
ServiceContract Job
static void ServiceContract(Args _args)
{
ClassBuild classBuild;
str header;
str newClassName = "AlexServiceContract";
;
header = 'public class '+ newClassName;
classBuild = new ClassBuild(newClassName);
classBuild.addMethod('classdeclaration', header + '\n{\n}\n');
classBuild.addMethod('createEntity', '[SysEntryPointAttribute(true)]' + '\n' + 'public void createEntity()' + '\n{\n}\n');
classBuild.addMethod('readEntity', '[SysEntryPointAttribute(true)]' + '\n' + 'public void readEntity()' + '\n{\n}\n');
classBuild.addMethod('updateEntity', '[SysEntryPointAttribute(true)]' + '\n' + 'public void updateEntity()' + '\n{\n}\n');
classBuild.addMethod('deleteEntity', '[SysEntryPointAttribute(true)]' + '\n' + 'public void deleteEntity()' + '\n{\n}\n');
classBuild.classNode().AOTcompile(1);
}
|
ServiceContract job simply creates Service Contract class with number of methods (createEntity, readEntity, updateEntity, deleteEntity). I'm going to implement readEntity method for data export below
DataContract Job
static void DataContract(Args _args)
{
ClassBuild classBuild;
str header;
str vars;
str newClassName = "AlexDataContract";
DictTable dictTable;
DictField dictField;
DictType dictType;
TableId tableId;
FieldId fieldId;
ExtendedTypeId typeId;
str tableName = "AlexTable";
str fieldName;
str typeName;
int i;
;
header = 'public class ' + newClassName;
classBuild = new ClassBuild(newClassName);
tableId = tableName2id(tableName);
dictTable = new DictTable(tableId);
if (dictTable)
{
for (i=1; i<= dictTable.fieldCnt(); i++)
{
fieldId = dictTable.fieldCnt2Id(i);
dictField = new DictField(tableId, fieldId);
if (dictField)
{
typeId = dictField.typeId();
if (typeId != 0)
{
dictType = new DictType(typeId);
if (dictType)
{
if (!dictField.isSystem())
{
fieldName = strFmt("%1", dictField.name());
typeName = strFmt("%1", dictType.name());
classBuild.addMethod(fieldName, '[DataMemberAttribute]' + '\n' +
'public ' + typeName + ' ' + fieldName + '(' + typeName + ' _' + fieldName + ' = ' + fieldName + ')' + '\n' +
'{' + '\n' +
'\t' + fieldName + ' = _' + fieldName + ';' + '\n' +
'\t' + 'return ' + fieldName + ';' + '\n' +
'}');
vars += '\t' + typeName + "\t" + fieldName + ';' + '\n';
}
}
}
}
}
}
classBuild.addMethod('classdeclaration', '[DataContractAttribute]' + '\n' + header + '\n{\n' + vars + '}\n');
classBuild.classNode().AOTcompile(1);
}
|
DataContract job is little bit more interesting because it allows to generate a full-blown Data Contract class with methods for all data elements based on a table (business entity) provided as input
As the result I got the following Service Contract and Data Contract classes
AlexDataContract
[DataContractAttribute]
public class AlexDataContract
{
AlexID AlexID;
AlexName AlexName;
}
|
[DataMemberAttribute]
public AlexID AlexID(AlexID _AlexID = AlexID)
{
AlexID = _AlexID;
return AlexID;
}
|
[DataMemberAttribute]
public AlexName AlexName(AlexName _AlexName = AlexName)
{
AlexName = _AlexName;
return AlexName;
}
|
AlexServiceContract
public class AlexServiceContract
{
}
|
[SysEntryPointAttribute(true)]
public void createEntity()
{
}
|
[SysEntryPointAttribute(true)]
public void deleteEntity()
{
}
|
[SysEntryPointAttribute(true)]
public void readEntity()
{
}
|
[SysEntryPointAttribute(true)]
public void updateEntity()
{
}
|
Now I'll implement Service Contract class readEntity method in order to export data from my custom table
readEntity method
[AifCollectionTypeAttribute('return', Types::Class, classStr(AlexDataContract)), SysEntryPointAttribute(true)]
public List readEntity()
{
AlexDataContract alexEntity;
AlexTable alexTable;
List alexList = new List(Types::Class);
while select alexTable
{
alexEntity = new AlexDataContract();
alexEntity.AlexID(alexTable.AlexID);
alexEntity.AlexName(alexTable.AlexName);
alexList.addEnd(alexEntity);
}
return alexList;
}
|
Technically at this point I have Service Contract and Data Contract classes implemented to the extend when I can use them for data export
Service Contract and Data Contract classes
Next I'll create a Custom Web Service based on Service Contract class as shown below
Custom Web Service
Upon Custom Web Service creation I added all exposed at this point operations
Operations
Then I will assign Custom Web Service to Service Group for deployment
Service Group
After deployment I'll get an Inbound port created
Inbound port
Before I move on I'll quickly create a simple .NET Client program to test out my Custom Web Service
.NET Client
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AlexTestClient.ServiceReference1;
using System.Xml.Serialization;
using System.IO;
namespace AlexTestClient
{
class Program
{
static void Main(string[] args)
{
try
{
AlexServiceClient client = new AlexServiceClient();
CallContext context = new CallContext();
context.Company = "USMF";
AlexDataContract[] alexEntities = client.readEntity(context);
foreach (AlexDataContract alexEntity in alexEntities)
{
Console.WriteLine("Name:" + alexEntity.AlexName);
}
XmlSerializer serializer = new XmlSerializer(typeof(AlexDataContract[]));
TextWriter textWriter = new StreamWriter(@"C:\Users\Administrator\Documents\Alex\Alex.xml");
serializer.Serialize(textWriter, alexEntities);
textWriter.Close();
Console.WriteLine("Success!");
}
catch (Exception e)
{
Console.WriteLine(e.InnerException.Message);
}
Console.ReadLine();
}
}
}
|
Please note that in this .NET Client program I used XmlSerializer object in order to convert the data I read from Microsoft Dynamics AX 2012 (based on AlexDataContract) into a file (for the sake of simplicity XML file)
Let's review how this XML file looks like
XML file
Okay! My Custom Web Service works fine, so now we're going to create an Outbound port and leverage built-in File system adapter and Outbound transforms (XSLT) in order to export a CSV file.
So without further due I'll create an Outbound port
Outbound port
For data export scenario I'll only need readEntity operation
Select service operations
Also in order to automatically convert XML output into CSV output I'm going to use XSLT transform
Outbound transforms
For the sake of simplicity I'll provide a dummy XSLT transform for now
Manage transforms
You can find more info about Microsoft Dynamics AX 2012 Outbound exchange here: http://technet.microsoft.com/en-us/library/hh696875.aspx
In a nutshell in order to organize Outbound exchange you will need to use the following:
<![if !supportLists]>- <![endif]>Step 1: AifSendService to queue up record for processing
<![if !supportLists]>- <![endif]>Step 2: AifOutboundProcessingService to create actual AIF message
<![if !supportLists]>- <![endif]>Step 3: AifGatewaySendService to generate resulting file
But before I continue with WCF Custom Web Service approach I want to implement the same Outbound exchange by using AIF Document Service, so I'll be able to contrast differences between Outbound exchange using WCF Custom Web Service and Outbound exchange using AIF Document Service
In order to introduce AIF Document Service to support my scenario I can use AIF Create document service wizard available in Tools > AIF
First off I'll create a query to support AIF Document Service
Query
Then I'll run Create document service wizard to generate all necessary artifacts for AIF Document Service. Let's very quickly run through it
AIF Document Service Wizard (Welcome)
AIF Document Service Wizard (Select document parameters)
AIF Document Service Wizard (Select code generation parameters)
AIF Document Service Wizard (Generate code)
AIF Document Service Wizard (Completed)
As the result AxdAlexDocument project gets created with all necessary artifacts included
Project
I'll also need to resolve some compilation errors by removing unnecessary in my scenario methods
AxAlexTable class – cache methods
Here's how my AIF Document Service looks like
AIF Document Service
Similarly I'll include into to Service Group for deployment
Service Group
After my AIF Document Service is deployed I can create Outbound port
Outbound port
And expose service operations
Select service operations
You can find numerous example of functionalities in standard Microsoft Dynamics AX 2012 which submit a record to be processed through Outbound port. The interesting thing in our scenario is that we want to organize a continuous data export (data feed) process for integration. That's why I wrote a small X++ job to help me submit records for processing through Outbound port (Step 1)
X++ Job: Submit records using AIF Document Web Service
static void AlexSendJob(Args _args)
{
AifConstraint aifConstraint = new AifConstraint() ;
AifConstraintList aifConstraintList = new AifConstraintList();
AifActionId actionId;
AifEndpointList endpointList = new AifEndpointList();
AifOutboundPort outboundPort;
Query query;
query = new Query(queryStr(AlexQuery));
actionId = AifSendService::getDefaultSendAction(classnum(AlexDocumentService), AifSendActionType::SendByQuery);
aifConstraint.parmType(AifConstraintType::NoConstraint);
aifConstraintList.addConstraint(aifConstraint);
endpointList = AifSendService::getEligibleEndpoints(actionId, aifConstraintList);
AifSendService::submitFromQuery(actionId, endpointList, query, AifSendMode::Async);
}
|
Please note that default action is "AlexDocumentService.read" when submitting records based on query
When records are submitted we'll see the following entry in Queue manager
Queue manager
On Step 2 I'll use another simple X++ Job to create actual AIF message by using AifOutboundProcessingService
X++ Job
static void AlexOutboundProcessingJob(Args _args)
{
AifOutboundProcessingService aifOutboundProcessingService = new AifOutboundProcessingService();
aifOutboundProcessingService.run();
}
|
As the result we'll be able to see appropriate entry on History form
History
Then we can review Document log
Document log
And finally get to the XML message itself
XML
XML
<?xml version="1.0" encoding="utf-16"?>
<MessageParts xmlns="http://schemas.microsoft.com/dynamics/2011/01/documents/Message">
<AlexDocument xmlns="http://schemas.microsoft.com/dynamics/2008/01/documents/AlexDocument">
<DocPurpose>Original</DocPurpose>
<SenderId>USMF</SenderId>
<AlexTable class="entity">
<_DocumentHash>47d6ff0fc52fd53de82f9df0dc1d37e4</_DocumentHash>
<AlexID>1</AlexID>
<AlexName>Value 1</AlexName>
<RecId>5637144576</RecId>
<RecVersion>1</RecVersion>
</AlexTable>
<AlexTable class="entity">
<_DocumentHash>03935629a0cabf8606b2ae1d7e163830</_DocumentHash>
<AlexID>2</AlexID>
<AlexName>Value 2</AlexName>
<RecId>5637144577</RecId>
<RecVersion>1</RecVersion>
</AlexTable>
<AlexTable class="entity">
<_DocumentHash>b006c8c97d261b4bc5c4aea65b06aed7</_DocumentHash>
<AlexID>3</AlexID>
<AlexName>Value 3</AlexName>
<RecId>5637145329</RecId>
<RecVersion>1</RecVersion>
</AlexTable>
</AlexDocument>
</MessageParts>
|
On Step 3 in order to generate a file I'll use another simple X++ job
X++ job
static void AlexGatewaySendJob(Args _args)
{
AifGatewaySendService aifGatewaySendService = new AifGatewaySendService();
aifGatewaySendService.run();
}
|
As the result we'll see file created in the folder
File
XML
XML
<?xml version="1.0" encoding="UTF-8"?><Envelope xmlns="http://schemas.microsoft.com/dynamics/2011/01/documents/Message"><Header><MessageId>{126F755E-9E21-4A85-B187-344A73824778}</MessageId><Action>http://schemas.microsoft.com/dynamics/2008/01/services/AlexDocumentService/find</Action></Header><Body><MessageParts xmlns="http://schemas.microsoft.com/dynamics/2011/01/documents/Message"><AlexDocument xmlns="http://schemas.microsoft.com/dynamics/2008/01/documents/AlexDocument"><DocPurpose>Original</DocPurpose><SenderId>USMF</SenderId><AlexTable class="entity"><_DocumentHash>47d6ff0fc52fd53de82f9df0dc1d37e4</_DocumentHash><AlexID>1</AlexID><AlexName>Value 1</AlexName><RecId>5637144576</RecId><RecVersion>1</RecVersion></AlexTable><AlexTable class="entity"><_DocumentHash>03935629a0cabf8606b2ae1d7e163830</_DocumentHash><AlexID>2</AlexID><AlexName>Value 2</AlexName><RecId>5637144577</RecId><RecVersion>1</RecVersion></AlexTable><AlexTable class="entity"><_DocumentHash>b006c8c97d261b4bc5c4aea65b06aed7</_DocumentHash><AlexID>3</AlexID><AlexName>Value 3</AlexName><RecId>5637145329</RecId><RecVersion>1</RecVersion></AlexTable></AlexDocument></MessageParts></Body></Envelope>
|
Great! Experiment with AIF Document Service was successful, so now we'll come back to our WCF Custom Service and try to run through the same process to generate the file
On Step 1 we'll submit records for processing through Outbound port
X++ Job: Submit records using WCF Custom Web Service
static void AlexSendJob(Args _args)
{
AifConstraint aifConstraint = new AifConstraint() ;
AifConstraintList aifConstraintList = new AifConstraintList();
AifActionId actionId;
AifEndpointList endpointList = new AifEndpointList();
AifOutboundPort outboundPort;
Query query;
query = new Query(queryStr(AlexQuery));
actionId = "AlexService.readEntity";
aifConstraint.parmType(AifConstraintType::NoConstraint);
aifConstraintList.addConstraint(aifConstraint);
endpointList = AifSendService::getEligibleEndpoints(actionId, aifConstraintList);
AifSendService::submitFromQuery(actionId, endpointList, query, AifSendMode::Async);
}
|
Please note that I used actionId = "AlexService.readEntity" explicitly in the code above. This is because a default submit action when submitting based on query is meant to be "AlexService.read" which is dictated by AIF, and in my WCF Custom Web Service I used a custom operation called "readEntity"
After that in order to successfully submit records for processing I'll have to provide a valid argument for my custom operation "readEntity". This is dictated by AifSendService.getValidSendParameter method as shown below
AifSendService.getValidSendParameter method
static private Object getValidSendParameter(AifActionId actionId, Object sendParameter)
{
AifDocumentClassId parameterClassId;
AifEntityKeyList entityKeyList;
DictMethod dictMethod;
boolean isValid = false;
if (sendParameter == null)
throw error("@SYS118921");
dictMethod = AifParameterLookup::getActionMethod(actionId);
if (dictMethod.parameterCnt() == 1)
{
if (dictMethod.parameterType(1) == Types::Class)
{
parameterClassId = dictMethod.parameterId(1);
if (parameterClassId == classidget(sendParameter))
{
isValid = true;
}
else if ((parameterClassId == classnum(AifEntityKeyList)) && (classidget(sendParameter) == classnum(AifEntityKey)))
{
// If the operation has an entityKeyList parameter and the send parameter was an entity key,
// then convert to an entitykey list
entityKeyList = new AifEntityKeyList();
entityKeyList.addEntityKey(sendParameter);
sendParameter = entityKeyList;
isValid = true;
}
}
}
if (!isValid)
throw error("@SYS118922");
return sendParameter;
}
|
In order to bypass this issue I used AifQueryCriteria object as argument just like it is implemented in AIF Document Service read operation. This is how readEntity method will look like before I submit
readEntity method
[AifCollectionTypeAttribute('return', Types::Class, classStr(AlexDataContract)), SysEntryPointAttribute(true)]
public List readEntity(AifQueryCriteria _queryCriteria = null)
{
AlexDataContract alexEntity;
AlexTable alexTable;
List alexList = new List(Types::Class);
while select alexTable
{
alexEntity = new AlexDataContract();
alexEntity.AlexID(alexTable.AlexID);
alexEntity.AlexName(alexTable.AlexName);
alexList.addEnd(alexEntity);
}
return alexList;
}
|
As the result we'll be see the following entry in Queue manager
Queue manager
However we'll still need to do one thing left on Step 2. This time the issue is that Class List (return type) is not Serializable
Queue manager
It is another requirement dictated by AIF
AifDispatcher.serializeObject method
static private AifXml serializeObject(classId xmlSerializableClassId, Object object)
{
AifXmlSerializable serializableObject;
SysDictClass dictClass;
AifXml objectXml;
if (object == null)
{
objectXml = AifDispatcher::serializeNullObject(xmlSerializableClassId);
}
else
{
dictClass = new SysDictClass(xmlSerializableClassId);
if (!dictClass.isImplementing(classnum(AifXmlSerializable)))
throw error(strfmt("@SYS112335", classId2Name(xmlSerializableClassId)));
serializableObject = object;
objectXml = serializableObject.serialize();
}
return objectXml;
}
|
Please note that formally according to AIF return type class has to implement AifXmlSerializable interface
In order to resolve this issue I implemented a brand-new custom Serializable AlexDataContractItemList class. Then my readEntity method looked like this
readEntity method
[SysEntryPointAttribute(true)]
public AlexDataContractItemList readEntity(AifQueryCriteria _queryCriteria = null)
{
AlexTable alexTable;
AlexDataContractItem alexItem;
AlexDataContractItemList alexItemList = AlexDataContractItemList::construct();
while select alexTable
{
alexItem = new AlexDataContractItem();
alexItem.AlexID(alexTable.AlexID);
alexItem.AlexName(alexTable.AlexName);
alexItemList.addAlexItem(alexItem);
}
return alexItemList;
}
|
Please see implementation of Serializable AlexDataContractItemList class below. Please note that I serialized it "manually" by providing schemas, etc.
First I implemented AlexDataContractItem class which represents a single record
AlexDataContractItem class X++ implementation
class AlexDataContractItem implements AifXmlSerializable
{
AlexID AlexID;
AlexName AlexName;
#define.AlexNamespace('http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem')
#define.AlexItem('AlexItem')
#define.AlexData('AlexData')
#define.AlexRecord('AlexRecord')
#define.AlexID('AlexID')
#define.AlexName('AlexName')
}
|
public AlexID AlexID(AlexID _AlexID = AlexID)
{
AlexID = _AlexID;
return AlexID;
}
|
public AlexName AlexName(AlexName _AlexName = AlexName)
{
AlexName = _AlexName;
return AlexName;
}
|
void deserialize(AifXml xml)
{
}
|
public AifDocumentName getRootName()
{
return #AlexItem;
}
|
public AifXml getSchema()
{
str schema =
@'<?xml version="1.0" encoding="utf-8"?>
<xsd:schema targetNamespace="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"
xmlns:tns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<xsd:element name="AlexItem" type="tns:AlexItem"/>
<xsd:complexType name="AlexItem">
<xsd:sequence>
<xsd:element name="AlexData" type="tns:AlexData"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="AlexData">
<xsd:sequence >
<xsd:element name="AlexRecord" type="tns:AlexRecord" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="AlexRecord">
<xsd:sequence>
<xsd:element name="AlexID" type="xsd:string"/>
<xsd:element name="AlexName" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>';
return schema;
}
|
public AifXml serialize()
{
AifXml xml;
XmlTextWriter xmlWriter;
xmlWriter = XmlTextWriter::newXml();
xmlWriter.formatting(XmlFormatting::None);
xmlWriter.writeStartDocument();
xmlWriter.writeStartElement2(#AlexItem, #AlexNamespace);
xmlWriter.writeStartElement2(#AlexData, #AlexNamespace);
xmlWriter.writeStartElement2(#AlexRecord, #AlexNamespace);
xmlWriter.writeElementString(#AlexID, AlexID);
xmlWriter.writeElementString(#AlexName, AlexName);
xmlWriter.writeEndElement();
xmlWriter.writeEndElement();
xmlWriter.writeEndElement();
xmlWriter.writeEndDocument();
xml = xmlWriter.writeToString();
xmlWriter.close();
return xml;
}
|
And then I implemented AlexDataContractItemList class which represents a list of records which exactly what I need as readEntity method return type
AlexDataContractItemList class X++ implementation
class AlexDataContractItemList implements AifXmlSerializable
{
Array alexArray;
int alexItemCount;
#define.AlexNamespace('http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItemList')
#define.AlexItemList('AlexItemList')
}
|
public void addAlexItem(AlexDataContractItem _alexItem)
{
alexArray.value(alexItemCount+1, _alexItem);
alexItemCount++;
}
public AlexDataContractItem getAlexItem(int _index)
{
return alexArray.value(_index);
}
|
public int getAlexItemCount()
{
return alexItemCount;
}
|
public void deserialize(AifXml xml)
{
}
|
public AifDocumentName getRootName()
{
return #AlexItemList;
}
|
public AifXml getSchema()
{
return
@'<?xml version="1.0" encoding="utf-8"?>
<xsd:schema targetNamespace="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItemList"
xmlns:tns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItemList"
xmlns:ek="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem" schemaLocation="AlexItem.xsd"/>
<xsd:element name="AlexItemList" type="tns:AlexItemList"/>
<xsd:complexType name="AlexItemList">
<xsd:sequence>
<xsd:element minOccurs="0" maxOccurs="unbounded" ref="ek:AlexItem" />
</xsd:sequence>
</xsd:complexType>
</xsd:schema>';
}
|
void new()
{
alexArray = new Array(Types::Class);
}
|
public AifXml serialize()
{
AifXml xml;
AifXml alexItemXml;
XmlTextWriter xmlWriter;
XmlTextReader xmlReader;
int x;
xmlWriter = XmlTextWriter::newXml();
xmlWriter.formatting(XmlFormatting::None);
xmlWriter.writeStartDocument();
xmlWriter.writeStartElement2(#AlexItemList, #AlexNamespace);
for(x=1; x <= this.getAlexItemCount(); x++)
{
alexItemXml = this.getAlexItem(x).serialize();
if(alexItemXml)
{
try
{
xmlReader = XmlTextReader::newXml(alexItemXml);
xmlReader.whitespaceHandling(XmlWhitespaceHandling::None);
xmlReader.moveToContent();
xmlWriter.writeRaw(xmlReader.readOuterXml());
xmlReader.close();
}
catch(Exception::Error)
{
throw error(strfmt("Unable to serialize item"));
}
}
}
xmlWriter.writeEndElement();
xmlWriter.writeEndDocument();
xml = xmlWriter.writeToString();
xmlWriter.close();
return xml;
}
|
public static AlexDataContractItemList construct()
{
return new AlexDataContractItemList();
}
|
You can find inspiration when implementing Serializable classes by looking at standard EntityKey and EntityKeyList classes in Microsoft Dynamics AX 2012
As the result after Step 3 file will be generated
XML - Record
XML - Record
<?xml version="1.0" encoding="UTF-8"?><Envelope xmlns="http://schemas.microsoft.com/dynamics/2011/01/documents/Message"><Header><MessageId>{03C75358-C243-4997-AD87-4F37F4E66ADE}</MessageId><Action>http://schemas.microsoft.com/dynamics/2008/01/services/AlexService/readEntity</Action></Header><Body><MessageParts xmlns="http://schemas.microsoft.com/dynamics/2011/01/documents/Message"><AlexItem xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"><AlexData><AlexRecord><AlexID>1</AlexID><AlexName>Value 1</AlexName></AlexRecord></AlexData></AlexItem></MessageParts></Body></Envelope>
|
XML – List of records
<?xml version="1.0" encoding="UTF-8"?><Envelope xmlns="http://schemas.microsoft.com/dynamics/2011/01/documents/Message"><Header><MessageId>{21614C10-265B-47E0-97A8-E83B0BD0177A}</MessageId><Action>http://schemas.microsoft.com/dynamics/2008/01/services/AlexService/readEntity</Action></Header><Body><MessageParts xmlns="http://schemas.microsoft.com/dynamics/2011/01/documents/Message"><AlexItemList xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItemList"><AlexItem xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"><AlexData><AlexRecord><AlexID>1</AlexID><AlexName>Value 1</AlexName></AlexRecord></AlexData></AlexItem><AlexItem xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"><AlexData><AlexRecord><AlexID>2</AlexID><AlexName>Value 2</AlexName></AlexRecord></AlexData></AlexItem><AlexItem xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"><AlexData><AlexRecord><AlexID>3</AlexID><AlexName>Value 3</AlexName></AlexRecord></AlexData></AlexItem><AlexItem xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"><AlexData><AlexRecord><AlexID>4</AlexID><AlexName>Value 4</AlexName></AlexRecord></AlexData></AlexItem><AlexItem xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/AlexItem"><AlexData><AlexRecord><AlexID>5</AlexID><AlexName>Value 5</AlexName></AlexRecord></AlexData></AlexItem></AlexItemList></MessageParts></Body></Envelope>
|
Finally after we apply Outbound transformation (XSLT) we can get to the final result
CSV
"", "1", "Value 1"
"", "2", "Value 2"
"", "3", "Value 3"
|
Now before we wrap up I want to discuss a few more things critical for this scenario:
<![if !supportLists]>- <![endif]>How to automate data export and organize continuous integration by using Batch jobs
<![if !supportLists]>- <![endif]>How to continuously generate a file with the same name
<![if !supportLists]>- <![endif]>How to apply XSLT transform in order to convert XML file into CSV file
Speaking about Batch jobs we can certainly establish a set of Batch jobs to periodically execute Step 1, Step 2 and Step 3 in batch. For this purpose you can establish universal AIFProcessing batch job to handle outbound and inbound (if needed) messages as shown below
Batch jobs
Batch tasks
Please note that you can also create a Batchable class Step 1 and include it into consolidated Batch job
Now let's switch to how to continuously generate a file with the same name
Please note that when you generate a file using File system adapter AIF will assign it a unique name (for example, "20140501_185022_0330838_00001.xml"). This is dictated by AIF and it happens during integration channel processing as shown below
AifFileSystemSendAdapter.getNewOutFileName method
private str getNewOutFileName()
{
str filename;
str curTimestamp;
str counterText;
int fileNumberMaxLength = 5;
// BP Deviation Documented
curTimestamp = fileSystem.GetCurrentTimestamp();
counter = (prevTimestamp == curTimestamp)? counter + 1:1;
prevTimestamp = curTimestamp;
counterText = strrep(int2str(0), fileNumberMaxLength) + int2str(counter);
filename = curTimestamp + #FileNameSeparatorCharacter + substr(counterText, strlen(counterText) - fileNumberMaxLength + 1, fileNumberMaxLength) + #XmlFileExtension;
return filename;
}
|
Here's how generated file may look like
File name
In order to consistently generate file with the same name you will have to implement appropriate business logic based on your requirements. In my case for the sake of simplicity I hardcoded file name to be "Alex.txt"
AifFileSystemSendAdapter.getNewOutFileName method
private str getNewOutFileName()
{
str filename;
str curTimestamp;
str counterText;
int fileNumberMaxLength = 5;
// BP Deviation Documented
curTimestamp = fileSystem.GetCurrentTimestamp();
counter = (prevTimestamp == curTimestamp)? counter + 1:1;
prevTimestamp = curTimestamp;
counterText = strrep(int2str(0), fileNumberMaxLength) + int2str(counter);
filename = curTimestamp + #FileNameSeparatorCharacter + substr(counterText, strlen(counterText) - fileNumberMaxLength + 1, fileNumberMaxLength) + #XmlFileExtension;
filename = "Alex.txt";
return filename;
}
|
As the result I can generate a file with needed name
File name
The only issue you will have to overcome though will be "File already exists" issue when you generate your file over and over again (more than once)
Exceptions
This issue occurs because of exception which gets generated in AifFileSystemSendAdapter.commit method
AifFileSystemSendAdapter.commit method
void commit()
{
str wipFileName;
if (!adapterIntialized)
throw error("@SYS95134");
if (outputFileWritten)
{
try
{
wipFileName = outputDirectory + currentWipFileName;
// BP Deviation Documented
fileSystem.MoveFile(wipFileName, outputDirectory + currentOutputFileName);
}
catch (Exception::CLRError)
{
throw error(strfmt("@SYS95804", wipFileName, AifUtil::getClrErrorMessage()));
}
outputFileWritten = false;
currentWipFileName = "";
currentOutputFileName = "";
}
}
|
In my example in order to overcome this issue I simply delete an old file if it exists in the folder where I'm going to put a new file
AifFileSystemSendAdapter.commit method
void commit()
{
str wipFileName;
if (!adapterIntialized)
throw error("@SYS95134");
if (outputFileWritten)
{
try
{
wipFileName = outputDirectory + currentWipFileName;
if (fileSystem.FindFiles(outputDirectory, currentOutputFileName))
fileSystem.DeleteFile(outputDirectory + currentOutputFileName);
// BP Deviation Documented
fileSystem.MoveFile(wipFileName, outputDirectory + currentOutputFileName);
}
catch (Exception::CLRError)
{
throw error(strfmt("@SYS95804", wipFileName, AifUtil::getClrErrorMessage()));
}
outputFileWritten = false;
currentWipFileName = "";
currentOutputFileName = "";
}
}
|
At the end I want to discuss XSLT transforms required to convert XML file to CSV file. In the example above I used a dummy XSLT transform which still extracted text from original XML. This happens because XSLT engines use so called built-in templates which automatically find nodes that are not specifically matched by any template rule. These built-in templates automatically find text among other things in the XML source when no explicit template matches that text, that's why I could extract text from XML even without having a meaningful XSLT transform
Dummy XSLT transform
<stylesheet version="1.0"
xmlns="http://www.w3.org/1999/XSL/Transform">
<output method="text"/>
</stylesheet>
|
Result
"{AA4A06AF-0FBE-45AA-A5DE-CFED502922F6}",
"http://schemas.microsoft.com/dynamics/2008/01/services/AlexService/readEntity"
"1Value 12Value 23Value 3"
|
Please note that some unnecessary header elements were also extracted into result file
Instead you can build a meaningful XSLT transform which will generate you CSV file in expected format
Manage transforms
For example, the following XSLT transform converts XML file into CSV file
XML Input
<AlexData>
<AlexRecord>
<AlexID>1</AlexID>
<AlexName>Value 1</AlexName>
</AlexRecord>
<AlexRecord>
<AlexID>2</AlexID>
<AlexName>Value 2</AlexName>
</AlexRecord>
<AlexRecord>
<AlexID>3</AlexID>
<AlexName>Value 3</AlexName>
</AlexRecord>
</AlexData>
|
XSLT transform
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="iso-8859-1"/>
<xsl:strip-space elements="*" />
<xsl:template match="/*/child::*">
<xsl:for-each select="child::*">
<xsl:if test="position() != last()">"<xsl:value-of select="normalize-space(.)"/>", </xsl:if>
<xsl:if test="position() = last()">"<xsl:value-of select="normalize-space(.)"/>"<xsl:text>
</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
|
CSV Output
"1", "Value 1"
"2", "Value 2"
"3", "Value 3"
|
Remark: In this article I used AIF Outbound port and File system adapter to organize data export. Depending on your requirements you can certainly use Web Services as is and implement your own integration client, for example, custom .NET integration client which reads info from Microsoft Dynamics AX 2012 via Inbound port and generates a CSV file. However please note that in this case you will have to implement file generation logic, data logging, XSLT engine interaction, advanced security (if needed) and other functionalities manually, as opposed to using AIF when all of the above comes with the framework
Summary: This document describes how to implement File Exchange data export integration with Microsoft Dynamics AX 2012 using Web Services and Outbound ports. In particular I contrasted experience you would have when using WCF Custom Web Services and AIF Document Services. I also focused on several important aspects for continuous integration (data export) such as process automation using batch jobs, generation of files with predefined names, etc.
Tags: Dynamics ERP, Microsoft Dynamics AX 2012, Integration, File Exchange, Data Export, WCF Custom Services, AIF Document Services, XML, CSV, XSLT.
Note: This document is intended for information purposes only, presented as it is with no warranties from the author. This document may be updated with more content to better outline the concepts and describe the examples.
Call to action: If you liked this article please take a moment to share your experiences and interesting scenarios you came across in comments. This info will definitely help me to pick the right topic to highlight in my future blogs
I am trying to create a sales order with One time customer using AIF , however I am not able to check the below Check box (One time customer) ,can you please help
ReplyDeletehttps://community.dynamics.com/resized-image.ashx/__size/550x0/__key/CommunityServer-Discussions-Components-Files/33/5826.New-Address.JPG
Hi Gaurav!
DeleteIn standard AX you have to make sure you select Default One-time customer account in Accounts Receivable Parameters > General > Customer > Default One-time customer account first. Then you can mark "On-time customer" checkbox upon a new Sales order creation
Also based on the screenshot provided it seems you have some AX customizations in place, so also you will have to make sure no other customization is changing the way standard One-time customer feature works in AX
Best Regards,
/Alex
Hi Alex,
DeleteThanks for your reply.
I have done the setup as per your suggestion , however the problem is that
AxdEntity_TableDlvAddr tableDlv = new AxdEntity_TableDlvAddr();
this enity is not having datamethods for "Role" , which is neccssary for one time customer , as soon as you mark "one time customer in Address form , Role is set as One Time and system prevents that entry to have relation with Customer but only with SalesOrder.
Can you please have a look and help .......
SalesOrderServiceClient proxy = new SalesOrderServiceClient();
Deleteproxy.ClientCredentials.Windows.ClientCredential.UserName = "Administrator";
proxy.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";
proxy.ClientCredentials.Windows.ClientCredential.Domain = "Contoso.com";
CallContext context = new CallContext();
context.Company = "USMF";
AxdSalesOrder salesOrder = new AxdSalesOrder();
AxdEntity_SalesTable[] salesTables = new AxdEntity_SalesTable[1];
AxdEntity_SalesTable salesTable = new AxdEntity_SalesTable();
AxdEntity_TableDlvAddr tableDlv = new AxdEntity_TableDlvAddr();
salesTable.CurrencyCode = "USD";
salesTable.CustAccount = "0000002";
salesTable.ReceiptDateRequested = Convert.ToDateTime("2/1/2012");
salesTable.PurchOrderFormNum = "PO113";
tableDlv.action = AxdEnum_AxdEntityAction.create;
tableDlv.CountryRegionId = "IND";
tableDlv.ZipCode = "110007";
salesTable.TableDlvAddr = new AxdEntity_TableDlvAddr[1] {tableDlv};
salesOrder.SalesTable = new AxdEntity_SalesTable[1] { salesTable };
try
{
proxy.create( context,salesOrder);
Console.WriteLine("Worked");
}
catch (Exception e)
{
throw e;
}
In this I dont have any way to mark that One time Checkbox
please help
ReplyDeleteHi Alex, This post has helped me tremendously. The only problem I am having is trying to serialize a header-line structure. No matter what I do the first header is created and all the lines from multiple headers below it. It never renders the next header. I am using separate data contracts for the header and lines but am not sure how to lay it out in the getschema and serialize methods of the List data contract. Any help would be greatly appreciated. Thanks, Rob
ReplyDeleteHi
ReplyDeletethe schema from the AxleItemList cannot be validated which makes the deployment of the serice fail in AX2012R3 - any solution to that ?
This comment has been removed by the author.
DeleteHi
DeleteI managed to craete a valid XML/XSD schema.... I cannot publish it here due to the blog software
Hi,
Deletecan u link your solution via pastebin or post it here directly?
Hi Alex! Your articles are invaluable and much appreciated!
ReplyDeleteDo you have an article describing how to consume an external web service (strongly-typed service client) inside a custom outbound HTTP adapter?
Similar to what Martin Dráb's mentioned here:
https://community.dynamics.com/ax/f/33/t/122653#259416
Eugen Glasow's Outbound HTTP adapter for AIF:
http://blogs.msdn.com/b/dynamics-coe/archive/2015/02/19/outbound-http-adapter-for-aif.aspx
Excellent post! However I would like to do the opposite and import a file using the File system adapter and a custom Service, to import data and execute business logic. It is possible to setup, I have tried every conceivable format of the input file and it never works.
ReplyDeleteAny ideas would be appreciated!
Excellent post! However I would like to do the opposite and import a file using the File system adapter and a custom Service, to import data and execute business logic. It is possible to setup, I have tried every conceivable format of the input file and it never works.
ReplyDeleteAny ideas would be appreciated!
Hi Alex! Your articles are invaluable and much appreciated!
ReplyDeleteCould you please explain how to create Outbound service for "HcmWorker" with 4 fields (Personnel number,Name,Position Id, Starting date)
Thank you.
Hi Alex i have an error after generate service 'Generated Xml Schema for class AlexDataContractItemList has errors. Error message: Cannot resolve the 'schemaLocation' attribute..' How can i fix it?
ReplyDelete