Walkthrough: Configure XML/File Data Export via AIF in AX 2012

This is an example how to export the LedgerJournalTable + LedgerJournalTrans records as XML via AIF

Create a Query

  1. Create a new query and name it AxdLedgerJournal.
  2. Place the LedgerJournalTable as main Datasource
  3. Set the Fields Property Dynamic to Yes
  4. Add the LedgerJournalTrans as child data source
  5. Set the Fields property Dynamic of the LedgerJournalTrans data source to Yes
  6. Set the Relation property of the LedgerJournalTrans data source to Yes
  7. Set the Fetch Mode property of the LedgerJournalTrans data source to 1:n
  8. Set the Join Mode property of the LedgerJournalTrans data source to Outer Join

Document Query

 

Use the AIF Document Wizard to generate the service classes

  1. In the menu bar go to Tools > Application Integration Framework > Create document service
  2. Provide the AxdLedgerJournal query as source query for the document service
  3. Select  the options to generate the Read and Find methods
  4. Select the option to generate the AxBC classes
  5. Let the wizard generate the software artifacts for you

AIF Wizard

Fine tune the generated code

  1. Go to the private projects and open the newly generated project
  2. Compile the project
  3. If the cache() methods don’t compile delete them
  4. Review the open tasks and provide meaningful labels
    Fine tune AIF Code

Configure the Batch Jobs

  1. In AX go to System Administration > Inquires > Batch Jobs > Batch Jobs
  2. Create a new Batch Job and go to “View Task”
  3. Add four task and choose the following classes
  4. AifGatewayReceiveService
  5. AifGatewaySendService
  6. AifInboundProcessingService
    AifOutboundProcessingService
  7. Close the Task window and set the Recurrence of the Batch Job to run e.g. all 5 minutes

AIF Batch Jobs

 

Configure the File System Adapter

  1. In AX go to System Administration > Setup > Services and Application Integration Famework > Outbound Ports
  2. Create a new Outbound Port
  3. Select the FileSystemAdapter as Adapter
  4. Provide a target location where to place the files e.g. a share with write permissions for the AOS account
  5. Open the Service Operation Dialog and add the LedgerJournalService.read method
  6. Activate the Port

AIF Outbound Port with File System Adapter

Add a send method

On the LedgerJournalTable add the following method

server void sendElectronically(XMLDocPurpose _xmlDocPurpose,
                               AifSendMode _aifSendMode = AifSendMode::Async)
{
    AxdSendContext      axdSendContext = AxdSendContext::construct();
    AifEntityKey        aifEntityKey   = AifEntityKey::construct();
    Map                 keyData;
    AifConstraint       aifConstraint = new AifConstraint() ;
    AifConstraintList   aifConstraintList = new AifConstraintList();
    ;
    keyData = SysDictTable::getKeyData(this);

    aifEntityKey.parmTableId(this.TableId);
    aifEntityKey.parmRecId(this.RecId);
    aifEntityKey.parmKeyDataMap(keyData);

    axdSendContext.parmXMLDocPurpose(_xmlDocPurpose);
    axdSendContext.parmSecurity(false);

    aifConstraint.parmType(AifConstraintType::NoConstraint);
    aifConstraint.parmId(this.JournalNum);
    aifConstraintList.addConstraint(aifConstraint);

    AifSendService::submitDefault(
        classNum(LedgerJournalService),
        aifEntityKey,
        aifConstraintList,
        _aifSendMode,
        axdSendContext.pack());   
}

Test the export

Create a job and use the following code to test the export

static void JobTestAifExport(Args _args)
{
    LedgerJournalTable journal;
    ;
    journal = ledgerJournalTable::find("00001");
    journal.sendElectronically(XMLDocPurpose::Original);
}

In AX got to System Administration > Periodic > Services and Application Integration > Queue Manager. You will find a record for the LedgerJournal export. The Batch Job will first create an XML Document and in a second step perform the export to the file system.

AIF Queue Manager

Add a calculated field to an AIF Document

AIF is great for application integration and providing external applications required data. Document exports can be generated very easily by providing a query to the AIF document wizard. However, if you want to provide not only table fields but calculated values (e.g. display methods) in an AIF document some work is required. This is an example how to add the CustInvoiceJour.contributionMargin() method to the SalesSalesInvoiceService.

Parameter methods

Add the following methods to the AxCustInvoiceJour class. (You may duplicate the parm* and set* method from an existing field e.g. InvoiceAmount and change the name and parameter.)

public AmountMst parmContributionMargin(AmountMst _margin = 0)
{
;
return custInvoiceJour.contributionMargin();
}

protected void setContributionMargin()
{
;
return;
}

Add a macro in the class declaration of the SaleSalesInvoice_CustInvoiceJour class

class SalesSalesInvoice_CustInvoiceJour extends AfStronglyTypedDataContainer
{
#define.XMLDocPurpose(‘XMLDocPurpose’)
#define.Weight(‘Weight’)
#define.Volume(‘Volume’)
// lot more here ..

    #define.ContributionMargin(‘ContributionMargin’)

}

Add the following methods to the SaleSalesInvoice_CustInvoiceJour class

public boolean existsContributionMargin()
{
return this.exists(#ContributionMargin);
}

public AmountMST parmContributionMargin(AmountMST_value = 0)
{
;
return this.get_Attribute(#ContributionMargin);
}

Refresh the schema definitions

Open the AIFDocumentSchemaTable in the table browser and delete the record for the DocumentName SalesInvoice.

Delete the SalesInvoice record from the AIFDocumentSchemaTable

In the AOT navigate to the SalesSalesInvoiceService. From the context menu choose to register the service. This will populate the AIFDocumentSchemaTable with the new XML schema including the new field.

Don’t’ forget to activate the new field in your endpoint. Navigate to Basic > Setup > Application Integration Framework > Endpoints > Action Policies > Data Policies > Data Policies and active the new field.

Activate the new field in the AIF data policies

Now you’re ready to test your work. In this example navigate to Accounts Receivable > Inquiries > Journals > Invoice and click on send electronically. Depending on your AIF configuration, check the output. Your new field should be there. Here is the XML from AIF Queue Manager processing an AIF message to a File System Adapter:

ContributionMargin in CustInvoiceJour AIF document

Preserve data when AIF service call fails

Dynamics AX AIF is great for all kind of interfaces and integration with 3rd party applications. By default AIF has an built-in transaction mechanism that prevents your system to become inconsistent if something goes wrong during processing.

Here is an example of a simple method which writes message data and the current transaction level into a table

[SysEntryPointAttribute]
public void call(Description _message)
{
    ERPServiceTable serviceTable;

    serviceTable.Message = _message;
    serviceTable.Info = strFmt("%1", appl.ttsLevel());
    serviceTable.insert();
}

Here is the service call from a C# console application

var client = new ERPService.ERPServiceClient();
client.ClientCredentials.Windows.ClientCredential.Domain = "YOUR_DOMAIN";
client.ClientCredentials.Windows.ClientCredential.UserName = "YOUR_USER";
client.ClientCredentials.Windows.ClientCredential.Password = "YOUR_PASSWORD";

var context = new ERPService.CallContext();
context.MessageId = Guid.NewGuid().ToString();
client.call(context, "Service Call");

When you call the service the result looks like this: Notice, the transaction level is already 1 although there is no ttsbegin in code.

AIF service call

But what happens when something goes wrong during processing the service call (e.g. posting a journal fails). Here is as simple method which can be used to simulate such a situation:

[SysEntryPointAttribute]
public void callWithError(Description _message,boolean _error)
{
    this.call(_message);
    if(_error)
    {
        throw error("Exception!");
    }
}

When you call the service method with the error flag set to FALSE, the message is written to the table and stays there.

context.MessageId = Guid.NewGuid().ToString();
client.callWithError(context, "Service Call without error", false);

However if you call the method with the error flag set to TRUE a rollback is triggered and your data is gone!

try
{
        context.MessageId = Guid.NewGuid().ToString();
        client.callWithError(context, "Service Call 2 with error", true);
}
catch
{
        Console.WriteLine("The service failed as expected!");
}

The table contains only 2 entries for both successful service calls. The data from the third service call is gone.

Service call with error not saved

However, if you want to keep the initial data for a post mortem analysis, this behavior is a problem. One way to work around this rollback is to reduce the transaction level back to 0 by calling an additional ttscommit. This will ensure no rollback will delete the transmitted data. Finally, raise the ttslevel back to 1 for further processing.

[SysEntryPointAttribute]
public void callWithError(Description _message,boolean _error = false)
{
    this.call(_message);

    ttsCommit;
    ttsBegin;

   
    if(_error)
    {
        throw error("Exception!");
    }
}

When you call the service method with the error flag set to true, the method still fails. But the initial transmitted data is preserved and can be used for analysis or manual processing.

Service call with error preserverd

BTW.

A more relaxed way to address this issue is to separate interfaces from business logic. For example provide two methods, one to transmit the data and another to process the data after it was successfully transmitted. If a second method is not an option (for what reason ever) write a batch class which processes the data. However, if you require immediate processing and a second call is not feasible this workaround may help.

AIF Error: Activity not found & Service Re-Deployment fails

A customer recently reported a problem with AIF services. The AIF log at System Administration > Periodic > AIF > Exceptions shows failed AIF calls from an external system resulting in an exception “Activity … not found”. However, the service seems to be active and online.

Default Voodoo here is to deactivate and reactive the service. However, it turned out that the service could not be reactivated. Dynamics AX reported an error that no activities were found. A detailed look revealed that all activities for this service somehow disappeared

Missing activities in Dynamics AX AIF service

The solution is to open a development workspace, navigate to the service node in the AOT and (re-) register the service.

Re-register AIF service

After re-register the service from the AOT, all missing activities were available again within the service configuration. The service could be activated without any problems.

Activities in Dynamcis AX AIF service

Be aware that other services may also be affected by the same problem. Check if your reports are working, and if not check the BI Service. In case the BIService can not be activated, navigate in the AOT to the service node and re-register the SRSFrameworkService and SSASFrameworkService.

BI Service Error: SSASFrameworkService not generated, Instance of ‘SysSecRole’ could not be created

I recently faced another problem with SSRS reports. The BI Service (Administration > Setup > AIF > Incoming Ports) was down and could not be activated. Trying to re-activate the service resulted in an error telling me that the SSASFrameworkService could not be generated because an instance of service class “SysSecRole” could not be generated. AOS restart and Full IL Compilation didn’t solve the problem.

However, the solution was to re-deploy the AIF Service Group: AOT > Service Groups > BI Service > Context Menu > Deploy Service Group. Afterwards the BI Service could be activated and reports were working again.

AIF Error Microsoft.Dynamics.Ax.Xpp.InvalidRemoteCallException

There are many reasons why an AIF Service call might fail, and I also ran into one. The task was to refactor existing code that processes some business logic and renames a file to .OLD. The original code looked like this

public class ERPProcessFile
{
    Filename fileName;

    public static void main(Args args) 
    { 
        ERPProcessFile pf = new ERPProcessFile(args);
        pf.run();
    }

    public void new(Args _args)
    {
        if(_args && _args.parm())
            fileName = _args.parm();
        else
            throw error(error::missingParameter(null));
    }

    private void run()
    {
        #File
        FileIOPermission permission;

        if(fileName != "")
        {
            permission = new FileIOPermission(fileName,#io_write);
            permission.assert();       
            WinAPI::moveFile(fileName,strFmt(‘%1.old’,fileName));        
            CodeAccessPermission::revertAssert();
        }
    }
}

The code above was tested using a Job

static void Job1(Args _args)
{
    Args args = new Args();
    args.parm(@"C:\Users\Public\Documents\textfile.txt");

    ERPProcessFile::main(args);
}

The code was wrapped in a service class and published as AIF HTTP web service

class ERPProcessFileService
{
    [SysEntryPointAttribute]
    public void processFile(Filename _localFile)
    {
        Args args = new Args();
        args.parm(_localFile);

        ERPProcessFile::main(args);
    }
}

The service was published to IIS, and imported in Visual C# as Ax2012 namespace

var context = new Ax2012.CallContext();
context.MessageId = Guid.NewGuid().ToString();

var client = new Ax2012.ERPProcessFileServiceClient();
client.ClientCredentials.Windows.ClientCredential =
new System.Net.NetworkCredential("USER", "PW", "DOMAIN");
client.processFile(context,@"C:\Users\Public\Documents\textfile.txt");

The service call resulted in an exception

{"Exception of type ‘Microsoft.Dynamics.Ax.Xpp.InvalidRemoteCallException’ was thrown."}

The reason was the use of WinAPI class which does not work when executed on the AOS. However, the code works fine when tested using a Job, or called within a Form. The solution was easy; Checking if the code is executed on Server or Client and using WinAPI or WinAPIServer class.

public class ERPProcessFile
{
    Filename fileName;

    public static void main(Args args) 
    { 
        ERPProcessFile pf = new ERPProcessFile(args);
        if(xGlobal::clientKind() == ClientType::Client) 
            pf.run();
        if(xGlobal::clientKind() == ClientType::Server)
            pf.runServer();
    }

    public void new(Args _args)
    {
        if(_args && _args.parm())
            fileName = _args.parm();
        else
            throw error(error::missingParameter(null));
    }

    private void run()
    {
        #File
        FileIOPermission permission;

        if(fileName != "")
        {
            permission = new FileIOPermission(fileName,#io_write);
            permission.assert();       
            WinAPI::moveFile(fileName,strFmt(‘%1.old’,fileName));        
            CodeAccessPermission::revertAssert();
        }
    }

    private void runServer()
    {
        #File
        FileIOPermission permission;

        if(fileName != "")
        {
            permission = new FileIOPermission(fileName,#io_write);
            permission.assert();       
            WinAPIServer::copyFile(fileName,strFmt(‘%1.old’,
                                                   fileName));         
            WinAPIServer::deleteFile(fileName)
            CodeAccessPermission::revertAssert();
        }
    }

}

UserSessionService/GetPartitionKey

When you upgrade from Dynamics AX 2012 RTM / FP to R2 and start upgrade your reports in Visual Studio, your might run into an error telling you that the GetPartitionKey action was not found in the UserSessionService.

GetPartitionKey action not found

Partitions were introduced in R2 and therefore the UserSessionService has changed and got new methods. However, the WSDL interface definition is the old one and you need to update the interface description.

AifUserSessionService in AX 2012 RTM and FP AifUserSessionService in AX 2012 R2
AX 2012 FP AX 2012 R2

If you want to check the actual interface description you might want to look at the WSDL itself. The URL can be found at AX > System Administration > Services and AIF > Inbound Ports > User Session Service > WSDL URI

UserSessionService in Dynamics AX 2012 R2

You might open the XML and go all the way through the files until you got the operations, or just feed the URL to Visual Studio. Create a new C# console application, add a service reference and provide the URL. If your WSDL definition is out of date your service reference looks like this.

Check the UserSessionService WSDL in Visual Studio

Go back to Dynamics AX development environment. Compile the AifUserSessionService class in X++ and incremental IL. Make sure there are no errors in your application. Go to the services node in the AOT and select the AifUserSessionService. From the context menu click Add Ins > Register Service. Go to the service group node in the AOT. Select the UserSessionService group. From the context menu click Deploy Service Group. You will see the infolog popping up telling you that the .NET artifacts have been generated and deployed.

Deploy the UserSessionService group

Go back to Visual Studio and refresh the service reference. The service has the same WSDL URL as before but the inferface has changed. Now you should see more methods including the GetPartitionKey.

Check the UserSessionService WSDL in Visual Studio

Receive large texts via AIF

One of our customer’s business partner transmits data as XML file. However, they did not make it to use a structured xml document service, but send a string that (in most cases) contains a valid xml document. Therefore we’ve setup an AIF service, using a class that takes one str parameter called _txt and stores it at a memo field.

public void storeText(str _text)
{
    AifBigData bigData;
    ;
    bigData.Text = _text;
    bigData.insert();
}

However, the business partner recently told us they get an error message while transmitting data to the AIF service telling them the transmitted data is too large. We were able to reproduce the issues by calling the service using a larger random-generated text.

class Program
{
    static void Main(string[] args)
    {
        var client = new BigData.BigDataServiceClient();
        client.ClientCredentials.Windows.ClientCredential = new
         System.Net.NetworkCredential("user","pass123","domain");

        // works fine
        client.storeText("Hello World");

        // fails 😦
        string txt = GenerateBigText();
        client.storeText(txt);
    }

    private static string GenerateBigText()
    {
        var random = new Random(); 
        var sb = new StringBuilder();
        for (int i = 0; i < 61440; i++)
        {
            char c =  (char)random.Next(65, 122);
            sb.Append(c);
        }

        return sb.ToString();
    }
}

The second call results in an exception

The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://insideax.at/bigdata:_text. The maximum string content length quota (8192) has been exceeded while reading XML data. This quota may be increased by changing the MaxStringContentLength property on the XmlDictionaryReaderQuotas object used when creating the XML reader.

However, the exception is well explained and contains a solution proposal to set the MaxStringContentLength

Go to Basic > AIF > Services > Button Configure

image

Select Binding > basicHTTPBinding > ReaderQuotas > MaxStringContentLenght and set e.g. 2147483647

image

Save and generate the .NET artifacts. Open windows explorer and navigate to the AIF virtual directory e.g. C:\Program Files\Microsoft Dynamics AX\50\AifWebServices . Select the web.config and open it with internet explorer. Make sure the MaxStringContentLength value is set

image

With this configuration the service call will work and a large text message is stored properly in Dynamics AX

image

Dynamics AX 2009: FTP Adapter for AIF

The Application Integration Framework in Dynamics AX is an extensible framework for data transportation and reception. It support the separation of transport technology (e.g. MSMQ, XML/SOAP Webservices, Filesystem Share) security aspects (services, permissions, transported data) and data manipulation. Creating new adapters to support other transport technologies is simple. Here is an example to support FTP.

Develop AIF FTP Adapter

  • First, create a new parameter table AifFTPParameters to store FTP server name, user, password and directory (4 strings). Next create a setup form for the AifFTPParameters table and a display menu item. Add an OK command button. Add this code to the buttons clicked event:

    void clicked()
    {;
        super();
        // use the server name as selected value 
        element.closeSelect(Setup_ServerName.text());
    }

    FTP Parameters setup

  • Create a new AifFTPSendAdapter class that implementes the AifSendAdapter interface. You may create empty implementations for begin(), commit(), initialize(), rollback() and terminate(). But you really need to implement the sendMessage() method. I’m using the System.Net Classes to implement the FTP transfer.
     

    public void sendMessage(AifGatewayMessage gatewayMessage)
    {
        System.Text.Encoding encoding;
        System.Byte[] bytes;
        str requestStr = "";
        object ftpo;
        System.Net.FtpWebRequest ftpRequest;
        System.Net.NetworkCredential credential;
        str ftpUser;
        str ftpPass;
        System.IO.Stream requestStream;
        InteropPermission clrPermission =
         new InteropPermission(InteropKind::ClrInterop);
        ;

        clrPermission.assert();

        switch(gatewayMessage.parmEncoding())
        {
            case ‘UTF-8’:
                encoding = System.Text.Encoding::get_UTF8();
                break;
            default:
                throw error("Unsupported Encoding");
        }
        bytes = encoding.GetBytes(gatewayMessage.parmMessageXml());

        requestStr = strfmt(‘ftp://%1′,AifFTPParameters::find().ServerName);
        if(AifFTPParameters::find().Directory)
        {
            requestStr = strfmt(‘%1/%2’,
                                 requestStr,
                                 AifFTPParameters::find().Directory);
        }

        requestStr = strfmt(‘%1/%2%3’,
                              requestStr,
                              gatewayMessage.parmMessageId(),
                              ’.xml’);

        ftpo =  System.Net.WebRequest::Create(requestStr);
        ftpRequest = ftpo;

        ftpUser = AifFTPParameters::find().UserName;
        ftpPass = AifFTPParameters::find().Password;
        //BP deviation documented
        credential = new System.Net.NetworkCredential(ftpUser,ftpPass);
        ftpRequest.set_Credentials(credential);
        ftpRequest.set_ContentLength(bytes.get_Length());
        ftpRequest.set_Method(‘STOR’);

        requestStream = ftpRequest.GetRequestStream();
        requestStream.Write(bytes,0,bytes.get_Length());
        requestStream.Close();

        CodeAccessPermission::revertAssert();
    }

  • Create a new AifFTPAdapter class that implements AifIntegrationAdapter. This class is used by the AIF configuration interface in Dynamics AX to identify an outbound adapter and its configuration form.

    public AifIntegrationAdapterType getAdapterType()
    {;
        return AifIntegrationAdapterType::SendOnly;
    }

    public MenuItemNameDisplay getAddressDisplayMenuItem()
    {;
        // the AifFTPParameter forms display menu items name
        return ‘AifFTPParameters’;
    }

    public AifTransportAddress getCanonicalTransportAddress(AifTransportAddress transportAddress)
    {;
        return transportAddress;
    }

    public MenuItemNameDisplay getConfigurationDisplayMenuItem()
    {;
        return ”;
    }

    public LabelString getLabel()
    {;
        return "AIF FTP Adapter";
    }

    public AifReceiveAdapter getReceiveAdapter()
    {;
        return null;
    }

    public AifSendAdapter getSendAdapter()
    {;
        return new AifFTPSendAdapter();
    }

    public boolean isHosted()
    {;
        return true;
    }

    public void validateConfiguration(AifTransportAddress transportAddress,
                                      AifChannelDirection channelDirection)
    {;
        //TODO:Check or throw error
    }

      1. Register FTP Adapter and configure AIF

    1. Go to Basics > Setup > Application Integration Framework.
    2. In Local Endpoints make sure you have an endpoint configured
    3. In Transport Adapters create a new record, select AifFTPAdapter and mark it as active.
    4. In Channels, create a new outbound FTP Channel, provide server name and credentials.
      Mark the channel as active.
      Configure AIF FTP Adapter 
    5. In Services activate the SalesSalesInvoiceService
      Activate SalesSalesInvoiceService 
    6. In Endpoints create a new endpoint. Set the outbound channel to your FTP channel and set the local endpoint. Go to the Constraints tab and set “No Constraints” flag. Mark the endpoint as active.
      Create an endpoint with outbound FTP

      Click the Action Policies button and add the SalesSalesInvoiceService.read method
      Activate SalesSalesInvoiceService.read

      Click the Data Policies button and use the set button to enable all data fields for transportation.
      Set data policies for invoice

    7. Go to Basic > Inquiries > Batch jobs. Make sure to run a batch job that processes the AIF message send and receive tasks: AIFInboundProcessingService, AIFOutboundProcessingService, AIFGatewaySendService and AIFGatewayReceiveService.
      Setup AIF processing batch jobs
    8. Go to Accounts Receivable > Inquiries > Journals > Invoice. Select an invoice and use the Send Electronically button to put it in the transportation queue.
      Send invoice electronically using AIF
    9. Wait a few miniutes and take a look at your FTP space. There you should see the transmitted invoices as xml file.
      Invoice successfully uploaded

    Query NoYes fields in AIF 4.0

    found an interesting behavior in AX 4.0 AIF. Querying a document with a NoYes field e.g. InventLocation.Manual requires to use the value “Yes” to become TRUE but value “1” doesn’t work.

    var client = new InventLocationServiceSoapClient();
    client.ClientCredentials.Windows.ClientCredential.Domain = "insideax";
    client.ClientCredentials.Windows.ClientCredential.UserName = "AifUser";
    client.ClientCredentials.Windows.ClientCredential.Password = "********";

    var context = new DocumentContext();
    context.MessageId = Guid.NewGuid().ToString();
    context.SourceEndpoint = "EXTAPP";
    context.DestinationEndpoint = "APP";

    var query = new QueryCriteria();
    query.CriteriaElement = new CriteriaElement[1];
    query.CriteriaElement[0] = new CriteriaElement();
    query.CriteriaElement[0].DataSourceName = "InventLocation";
    query.CriteriaElement[0].FieldName = "Manual";
    query.CriteriaElement[0].Operator = Operator.Equal;
    query.CriteriaElement[0].Value1 = "1";

    var axdInventLocation = client.findListInventLocation(context, query);
    Console.WriteLine(axdInventLocation.InventLocation == null);

    // new guid
    context.MessageId = Guid.NewGuid().ToString();
    // change value
    query.CriteriaElement[0].Value1 = "Yes";

    axdInventLocation = client.findListInventLocation(context, query);
    Console.WriteLine(axdInventLocation.InventLocation == null);
    Console.ReadKey();

    Result is shown here:

    image