Saturday, May 3, 2014

Microsoft Dynamics AX 2012 – File Exchange (Export) using Web Services (WS)

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
 
<![if !vml]><![endif]>
 
 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>&#xD;</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.
 
Author: Alex Anikiev, PhD, MCP
 
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

15 comments:

  1. 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


    https://community.dynamics.com/resized-image.ashx/__size/550x0/__key/CommunityServer-Discussions-Components-Files/33/5826.New-Address.JPG

    ReplyDelete
    Replies
    1. Hi Gaurav!

      In 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

      Delete
    2. Hi Alex,
      Thanks 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 .......

      Delete
    3. SalesOrderServiceClient proxy = new SalesOrderServiceClient();
      proxy.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

      Delete
  2. Hi 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

    ReplyDelete
  3. Hi

    the schema from the AxleItemList cannot be validated which makes the deployment of the serice fail in AX2012R3 - any solution to that ?

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Hi

      I managed to craete a valid XML/XSD schema.... I cannot publish it here due to the blog software

      Delete
    3. Hi,
      can u link your solution via pastebin or post it here directly?

      Delete
  4. Hi Alex! Your articles are invaluable and much appreciated!
    Do 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

    ReplyDelete
  5. 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.

    Any ideas would be appreciated!

    ReplyDelete
  6. 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.

    Any ideas would be appreciated!

    ReplyDelete
  7. Hi Alex! Your articles are invaluable and much appreciated!
    Could you please explain how to create Outbound service for "HcmWorker" with 4 fields (Personnel number,Name,Position Id, Starting date)

    Thank you.

    ReplyDelete
  8. 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