AX 2012. High “System Id” Number Sequence consumption or how incorrect setup can affect AX.

Recently we did a performance analysis for one of our customers using DynamicsPerf utility. Analyzing number sequence consumption, to determine preallocation quantities, we noticed high system id usage in one of the companies (50 000 per day). We went through a list of possible entities that could consume this number sequence and realized that only sales order invoices are posted on a daily basis. However, the quantity of invoices is less than ten per day and could not be a case.

After additional investigation, we found a sales order invoice posting batch job scheduled with 1-hour recurrence.

This batch job had a late selection parameter checked and quite strange selection criteria:

SelectionCriteria

Bingo! As you can see here AX should process all orders irrespectively of status. In this company we had 10000 of posted sales orders, because of incorrect setup batch job iterates through each order and creates one batch task per 5 orders (depending on setup in accounts receivable parameters, that is 5 by default). Each batch task allocates new parmId from system id number sequence. So, 10000 orders will create 2000 tasks every hour that leads to 48000 tasks per day!

This is a good example how incorrect setup may add additional overhead to overall system performance creating thousands of batch tasks and doing thousands DB calls. And the fix is pretty simple – exclude invoiced orders from selection criteria.

 

AX 7. Using Timer to perform a periodic task.

In previous versions of AX, we could use setTimeOut() method to execute a form method periodically (actually this method belongs to Object, so any class derived from Object could benefit from it). However, this method is deprecated in AX 7 and new setTimeoutEx() is introduced to replace it.

You may find setTimeOut() in source code, but it does nothing, and probably should cause BP error or warning.

I created a sample form to demonstrate how it works. It has only one Time control that is updated each 500 milliseconds. This form is quite similar to AX 2012 tutorial_Timer but it does not exist in AX 7 anymore.

TestTimerForm

[Form]
public class TestTimer extends FormRun
{
    void run()
    {
        super();
        element.setTimeoutEx(formMethodStr(TestTimer, updateTime), conNull(), 500);
    }

    public void updateTime(AsyncTaskResult _result)
    {
        if (!element.closed()) //otherwise will be executed even after form close.
        {
            TimeControl.value(DateTimeUtil::getTimeNow(DateTimeUtil::getUserPreferredTimeZone()));
            element.setTimeoutEx(formMethodStr(TestTimer, updateTime), conNull(), 500);
        }
    }
}

When you call setTimeoutEx() method AX adds TimerControl control to a form and executes task asynchronously.

Please note two important things here:

  1. Method called by setTimeoutEx() should accept AsyncTaskResult as a parameter or run time exception will be thrown.
  2. updateTime() method checks if a form is not closed, otherwise AX will execute this code even after the form is closed.

AX 2012 R3. How to disable inventory dimension hashing to improve performance.

As all of you may know there is a SQL limitation on maximum number of fields in index. We cannot have more than 16 (actually maximum is 14 because index includes DataAreaId and Partition fields). It’s quite important for inventory dimensions story and this limitation was hit in AX 2012 R3 with new fields for advance warehouse.

To overcome this limitation new hash field is introduced.  This field contains hashed value of other dimension fields, so-called “secondary dimensions”.  Using new approach, we can add more than 14 dimensions but have to pay a performance overhead for this flexibility.

However, usually we don’t use all inventory dimensions like InventGtdId_RU, InventProfileId_RU and InventOwnerId_RU. So we can disable inventory extensibility to reduce the performance overhead.

Let’s do it step by step.

1. Remove unused fields from DimIdx index of InventDim table.

We don’t use Russian localization, so we will remove InventGtdId_RU, InventProfileId_RU, and InventOwnerId_RU fields from the index.

DimIdx

Also we need to remove SHA1Hash filed because we don’t need it any more.

We will add InventStatusId and LicensePlateId fields to DimIdx index because we are using advance warehouse.

DimIdxNewFields

2. Disable InventDimExtensibility configuration key that controls dimensions extensibility.

3. Modify InventDIm.hashKey() method to skip hash calculation on update and insert.

/// <summary>
/// Calculates the string that is used when the hash value is calculated for the dimension values that
/// are not included in the <c>DimIdx</c> index.
/// </summary>
/// <returns>
/// A string that contains the calculated hash value.
/// </returns>
public str hashKey()
{
    str     hashKey   = '';
    str     hashKeyCaseInsensitive = '';

    #InventDimDevelop

    //Disable invent dimension extensibility -->
    if (!isConfigurationkeyEnabled(configurationKeyNum(InventDimExtensibility)))
    {
        return hashKey;
    }
    //Disable invent dimension extensibility <--

    /*
    SQL Server has a limitation of 16 fields in one index. For InventDim this is a problem if extra dimensions are added in an installation.
    Instead we have added a new field which can store hashed values of the least used dimensions.
    The class InventDimUniquenessEnabling can be used to validate if the proper indexes are defined and this method includes the right fields.

    Create a string this is unique for every combination of the dimensions. This can for example be achieved by using the code pattern below for each field included in the hash.
    The field values must be trimmed for trailing spaces, as this method is invoked before insert() - where such trimming also occurs.

    if (this.<FieldName>)
    {
        hashKey += (hashKey ? '~' : '') + '<FieldName>:' + strRTrim(this.<FieldName>);
    }
    */

    // Due to index limitations, hash the values.
    if (this.LicensePlateId)
    {
        hashKeyCaseInsensitive += (hashKey ? '~' : '') + 'LicensePlateId:' + strRTrim(this.LicensePlateId);
    }

    if (this.InventStatusId)
    {
        hashKeyCaseInsensitive += (hashKeyCaseInsensitive ? '~' : '') + 'InventStatusId:' + strRTrim(this.InventStatusId);
    }

    return hashKey + strLwr(hashKeyCaseInsensitive);
}

4. Modify InventDIm.findDim() method to search by new index fields.

client server static public InventDim findDim(
    InventDim   _inventDim,
    boolean     _forupdate = false)
{
    // <GEERU>
    #ISOCountryRegionCodes
    // </GEERU>
    InventDim       inventDim;

    if (_forupdate)
    {
        inventDim.selectForUpdate(_forupdate);
    }

    // Fields might not have been selected on the specified buffers, or might have been updated since selection
    _inventDim.checkInvalidFieldAccess(false);

    if (isConfigurationkeyEnabled(configurationKeyNum(InventDimExtensibility)))
    {
        select firstonly inventDim
        where inventDim.ConfigId            == _inventDim.ConfigId
           && inventDim.InventSizeId        == _inventDim.InventSizeId
           && inventDim.InventColorId       == _inventDim.InventColorId
           && inventDim.InventStyleId       == _inventDim.InventStyleId
           && inventDim.InventSiteId        == _inventDim.InventSiteId
           && inventDim.InventLocationId    == _inventDim.InventLocationId
           && inventDim.InventBatchId       == _inventDim.InventBatchId
           && inventDim.wmsLocationId       == _inventDim.wmsLocationId
           && inventDim.wmsPalletId         == _inventDim.wmsPalletId
           && inventDim.sha1Hash            == _inventDim.hashValue() // Needed to hit unique index cache. All dimensions should be included in the where clause - also those included in the hash key,
           && inventDim.InventSerialId      == _inventDim.InventSerialId
           && inventDim.InventGtdId_RU      == _inventDim.InventGtdId_RU
           && inventDim.InventProfileId_RU  == _inventDim.InventProfileId_RU
           && inventDim.InventOwnerId_RU    == _inventDim.InventOwnerId_RU;
    }
    else
    {
        select firstonly inventDim
            where inventDim.ConfigId         == _inventDim.ConfigId
               && inventDim.InventSizeId     == _inventDim.InventSizeId
               && inventDim.InventColorId    == _inventDim.InventColorId
               && inventDim.InventStyleId    == _inventDim.InventStyleId
               && inventDim.InventSiteId     == _inventDim.InventSiteId
               && inventDim.InventLocationId == _inventDim.InventLocationId
               && inventDim.InventBatchId    == _inventDim.InventBatchId
               && inventDim.wmsLocationId    == _inventDim.wmsLocationId
               && inventDim.wmsPalletId      == _inventDim.wmsPalletId
               //Disable invent dimension extensibility -->
               /* Orig -->
               // <GEERU>
               && inventDim.InventGtdId_RU      == _inventDim.InventGtdId_RU
               && inventDim.InventProfileId_RU  == _inventDim.InventProfileId_RU
               && inventDim.InventOwnerId_RU    == _inventDim.InventOwnerId_RU
               // </GEERU>
               && inventDim.InventSerialId   == _inventDim.InventSerialId;
               Orig <-- */
               && inventDim.InventSerialId   == _inventDim.InventSerialId
               && inventDim.InventStatusId   == _inventDim.InventStatusId
               && inventDim.LicensePlateId   == _inventDim.LicensePlateId;
               //Disable invent dimension extensibility <--
    }
    #inventDimDevelop

    return inventDim;
}

5. Review all customization and standard code that use removed\added dimensions fields in where statement. This could be done using cross references.

That’s all!

To read more about promoting and demoting inventory dimensions please refer to this msdn article.

Also additional improvements were made in AX 7, for more details read this blog post.

AX7. Display methods and Form Observability.

In AX 2012 display methods are called every time when a form is redrawn, however this behaviour is changed in AX 7 to optimize performance in web client.

Using Form Observability, we can notify form that display method should be updated or recalculated when related variable, data source or control are updated.

There are three different scenarios:

  • Display method depends on a variable.
  • Display method depends on another data source.
  • Display method depends on an event.

Display method depends on a variable.

For example, we have a form level variable “valid, display method that shows a status on the form and button to update the status.

MyForm

[Form]
public class MyForm extends FormRun
{
    [FormObservable]
    boolean valid;

    display public boolean displayIsValid()
    {
        return valid;
    }

    [Control("Button")]
    class Validate
    {
        public void clicked()
        {
            super();
            valid = true;
        }
    }
}

When the form is opened display method will be executed and show default value (false) on the form. When the button is clicked AX does not execute display method so value on the form remains the same.

However variable is decorated with FormObservable attribute and AX will observe it and automatically execute display method when value of the variable is changed.

Form can observe form level variables and class level variables as well. For example, AssetChangeGroup class has couple of variables decorated with FormObservable attribute.

class AssetChangeGroup extends RunBase
{
    [FormObservable]
    AssetGroupId        assetGroupId;

    [FormObservable]
    NoYesId             changeAssetId;

    [FormObservable]
    AssetId             assetId;
}

This class is used in edit methods of AssetChangeGroup form.

[Form]
public class AssetChangeGroup extends FormRun
{
    edit AssetGroupId assetGroupId(boolean set, AssetGroupId _assetGroupId)
    {
        if (set)
        {
            assetChangeGroup.parmAssetGroupId(_assetGroupId);
            element.initAssetId();
            element.enableOrDisableAssetId();
        }

        return assetChangeGroup.parmAssetGroupId();
    }

So any update of class level variables will trigger update of edit methods on the form.

Display method depends on another data source.

In the next example we have a simple header-lines form.

HeaderLine

There is a display method on the header grid that shows total lines quantity.

[Form]
public class MyForm extends FormRun
{
    [DataSource]
    class MyHeader
    {
        public display Qty displayTotal(MyHeader _myHeader)
        {
            MyLine_DS.observe();
            MyLine  myLineLocal;

            select sum(Qty) from myLineLocal
                where myLineLocal.MyHeader == _myHeader.RecId;

            return myLineLocal.Qty;
        }
    }
}

Here MyLine_DS.observe() is used to observe MyLine_DS data source. AX will automatically execute display method if any record in the lines grid is updated, inserted or deleted.

Display method depends on an event.

In some cases, display method does not depend on a forms state or a data source. However, it should be refreshed on a specific event, like a button click or a method call. To handle this, we can use FormObservableLink object.

In the next example we have a form with a display method and a button that invokes a class method. This method will update  data that the display method depends on and invoke  the method on the form to notify the display method that it should be updated.

UpdateLines

Class does records update and calls a method on the form:

class MyClass
{
    public static void main(Args _args)
    {
        MyHeader myHeader = _args.record() as MyHeader;

        if (myHeader)
        {
            MyLine myLine; 

            ttsbegin;
            update_recordset myLine
            setting Qty = 10
                where myLine.MyHeader == myHeader.RecId;
            ttscommit;

            var caller = _args.caller();

            if (caller &amp;&amp; Global::formHasMethod(caller, formMethodStr(MyForm, refreshTotals)))
            {
                caller.refreshTotals();
            }
        }
    }
}

On the form linesTotalObservableLink.markChanged() is used to notify the form that it should execute the display method and linesTotalObservableLink.observe() to receive notification.

[Form]
public class MyForm extends FormRun
{
    FormObservableLink linesTotalObservableLink;

    public void refreshTotals()
    {
        linesTotalObservableLink.markChanged();
    }

    public void init()
    {
        super();
        linesTotalObservableLink = new FormObservableLink();
    }

    [DataSource]
    class MyHeader
    {
        public display Qty displayTotal(MyHeader _myHeader)
        {
            linesTotalObservableLink.observe();

            MyLine  myLineLocal;

            select sum(Qty) from myLineLocal
                where myLineLocal.MyHeader == _myHeader.RecId;

            return myLineLocal.Qty;
        }
    }
}

AX7. Get information about table extension using Microsoft.Dynamics.Ax.Xpp.MetadataSupport

In AX 2012 we used DictTable class to get information about tables, table fields, indexes and field groups. However we cannot retrieve any information about table extensions using this class, so Microsoft.Dynamics.Ax.Xpp.MetadataSupport was introduced in AX 7.

Let’s look at simple job that will return all table extensions and all fields from table extensions for CompanyInfo table.

public static void main(Args _args)
{
    var axTableExtensions = Microsoft.Dynamics.Ax.Xpp.MetadataSupport::GetTableExtensionsForTable(tableStr(CompanyInfo));
    System.Collections.IEnumerator axTableExtensionsEnumerator = axTableExtensions.GetEnumerator();

    while (axTableExtensionsEnumerator.moveNext())
    {
        Microsoft.Dynamics.AX.Metadata.MetaModel.AxTableExtension axTableExtension = axTableExtensionsEnumerator.get_Current();

        info(strFmt("Extension name %1", axTableExtension.Name));

        var axTableExtensionFields = axTableExtension.Fields;

        System.Collections.IEnumerator axTableExtensionFieldsEnumerator = axTableExtensionFields.GetEnumerator();

        while (axTableExtensionFieldsEnumerator.moveNext())
        {
            Microsoft.Dynamics.AX.Metadata.MetaModel.AxTableField axTableExtensionField = axTableExtensionFieldsEnumerator.get_Current();

            info(strFmt("Field name %1", axTableExtensionField.Name));
            info(strFmt("Mandatory %1", axTableExtensionField.Mandatory));
            info(strFmt("Allow edit %1", axTableExtensionField.AllowEdit));
        }
    }
}

Query::insert_recordset() ignores XDS policy.

Recently we did a customization to create custom XDS policy and found out that Query::insert_recordset() ignores it, so all data is selected.

Simple workaround is to rewrite Query::insert_recordset() to while(queryRun.Next()).

However no one really wants to modify bunch of standard code that uses this method so connect issue was created. Feel free to vote for it.

It could be reproduced in AX 2012 and AX 7 as well.

AX 2012. SysEmailDistributor does not send emails to multiply recipients.

We have a couple of modifications that use SysEmailTable::sendMail() to send emails and found that if you pass multiple recipients into _emailAddr parameter AX will send email only to last recipient.

This issue was introduced with SysMailerNet in R3. In previous version we were able to pass multiple email addresses using a semicolon as delimiter. Now each email address should be added using SysMailerNet.tos.add() method.

However SysMailerNet::quickSend() method does not have this issue, so we can use it as an example to fix SysEmailDistributor.

Let’s modify SysEmailDistributor::processEmails() method. In standard code we can see that recipient from outgoingEmailTable is passed to mailer.tos().

//instantiate email
mailer.fromAddress(outgoingEmailTable.Sender,outgoingEmailTable.SenderName);
tos = mailer.tos();
tos.add(outgoingEmailTable.Recipient);

To fix our issue we need to split emails and pass them to tos.add() in a loop.

//multiple recipients fix -->
List emailAddresses;
ListEnumerator enum;
//multiple recipients fix <--

…

//instantiate email
mailer.fromAddress(outgoingEmailTable.Sender,outgoingEmailTable.SenderName);
tos = mailer.tos();
/* Orig -->
tos.add(outgoingEmailTable.Recipient);
Orig <-- */
//multiple recipients fix -->
emailAddresses = SysEmailDistributor::splitEmail(outgoingEmailTable.Recipient);
enum = emailAddresses.getEnumerator();
while (enum.moveNext())
{
    tos.add(enum.current());
}
//multiple recipients fix <--
 

By the way this issue is fixed in AX 7.