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