MSDyn365FO. AX 2012 data upgrade with virtual companies.

In Dynamics 365 for Finance and Operations virtual companies are deprecated and could not be upgraded according to the documentation that just state this fact without proposing a solution.

If we cannot upgrade, then we need to get rid of them. Here are the high level steps how you can de-virtualize a table. Let’s say that we have virtual company V with two companies: A and B. V has only one table collection with one table for simplicity, let’s call it Table1.

  1. Delete a data from Table1 that belongs to the companies A and B. When you create a virtual company, you may already have some data in the tables you want to share. That data may be orphaned, so we want to delete it to avoid duplicates. Can be done via simple T-SQL script:
    DELETE FROM Table1
  2. Go to System administration > Setup > Virtual company accounts and delete the virtual company.
  3. Restart AX client.
  4. Insert data from the virtual company to de-virtualized companies via X++ job:
    static void deVirtualizeTables(Args _args)
        DataAreaId          virtualDataAreaId = 'V';
        container           oldCompaniesCon = ['A', 'B'];
        VirtualDataAreaList virtualDataAreaList;
        void deVirtualizeTable(TableId _tableId)
            int             i;
            DataAreaId      dataAreaId;
            SysDictTable    dictTable = new SysDictTable(_tableId);
            Common          buffer = dictTable.makeRecord();
            Common          newBuffer;
            while select crossCompany buffer
                where buffer.dataAreaId == virtualDataAreaId
                for (i = 1; i <= conLen(oldCompaniesCon); i++)
                    dataAreaId = conPeek(oldCompaniesCon, i);
                    changeCompany (dataAreaId)
                        newBuffer = null;
                        newBuffer = dictTable.makeRecord();
                        buf2buf(buffer, newBuffer);
        select firstOnly RecId from virtualDataAreaList;
        if (virtualDataAreaList)
            throw error('Delete a virtual company first!');
  5. Delete data from the virtual company. It cannot be done from X++ because you cannot use changeCompany with a company that does not exist. T-SQL:
    DELETE FROM Table1

Now we are ready to run standard data upgrade procedure!

One more reason to avoid global variables


You can find tons of articles and discussions on the web why global variables should be avoided in any programming language. Here is another one that is AX specific.

Recently, I had to deal with a heisenbug and when I got to the bottom of it, I realized that anyone could do the same rookie mistake. I did a quick search and found at least one in standard application, so it’s worth to share and maybe you need to fix it in your code as well!

This bug is related to SysGlobalObjectCache. It is recommended to use it for better performance. To create an entry in the cache we need scope, key and value. Usually we use fixed scope for each task type (i.e. CustVendExternalItemDescriptionExistsCheck). Key usually is a record Id we are searching in DB (i.e. “0001”) and value is a data returned from DB (i.e. InventTable record).  But we often forget that records in different legal entities could have same id and result cached in one company is not valid for another company!

Let’s look at an example. On standard demo data I found an item that has same id (0001) in 2 companies (usrt, frrt). I added external description for it only in one company (usrt). Now I’m using standard findExternalDescription() method of VendExternalItemDescription class that returns external item description. Under the hood it checks if there is a record for given item and vendor combination and caches the result to avoid extra database calls.

In the test job bellow first call to findExternalDescription() method caches false because there is no description for given key. Then I switch to the company where I have a description, but method still returns false because there is an entry in the cache. When the cache is cleared method finally returns true because it does actual search in DB.

class RunnableClass1
    public static void main(Args _args)
        ItemId itemId = '0001';
        VendAccount vendAccount = '1001';
        inventDimId inventDimId = 'AllBlank';

            //search for an item description. False is cached because no description exists
            boolean found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();

        //change company to company where description exists
            boolean found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();
            //false is returned because it is cached from previous call

            //reset cache

            found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();
            //true is returned after cache flush

As you can see, it’s very easy to create similar issue or maybe you already have one, because it is data specific and we rarely test our customizations using multiple companies. From another side, it is even easier to fix it, just add DataAreaId to the cache key!

Advanced cross-reference search.

Cross-references search

Cross-reference is one of the best tools we have in AX that is used by developers daily, however, not everyone is using it for 100%.

Recently, I was asked how to find cross-references for a kernel method that is not defined on a table, because you cannot right click on it 😊   And that’s a good question, often we want to find if there is a call to doUpdate or doInsert somewhere.

In AX 2012 go to Tools -> Cross-reference -> Names and filter by table or method name and then click Used by.



Using this form, we can find usage of CRL types as well, for example, Microsoft.Dynamics.IntegrationFramework.  It is defined in AOT under references node, however, from there you cannot find any references.  Names would show you everything!


What about D365FOE?

As we know, it’s a bit different here. Cross-references data is moved to DYNAMICSXREFDB database.


And it makes perfect sense because you need to pay for each GB of DB space in Production.

Using next SQL statement, we can find usage of any object. For example, Microsoft.Dynamics.IntegrationFramework :

	sourceName.[Path] as sourcePath,
	targetName.[Path] as targetPath,
	sourceModule.[Module] as sourceModule,
	targetModule.[Module] as targetModule
	FROM dbo.[References]
	INNER JOIN dbo.[Names] sourceName
		ON dbo.[References].[SourceId] = sourceName.[Id]
	INNER JOIN dbo.[Modules] sourceModule
		ON sourceName.[ModuleId] = sourceModule.[Id]
	INNER JOIN dbo.[Names] targetName
		ON dbo.[References].[TargetId] = targetName.[Id]
	INNER JOIN dbo.[Modules] targetModule
		ON targetName.[ModuleId] = targetModule.[Id]
	WHERE targetName.[Path] like '%Microsoft.Dynamics.IntegrationFramework%'


Where Kind is:

    /// <summary>
    /// Types of Cross References
    /// </summary>
    public enum CrossReferenceKind
        /// <summary>
        /// Type not specified. Used for queries
        /// </summary>
        Any = 0,

        /// <summary>
        /// Indicates that the reference is a Method Call
        /// </summary>
        MethodCall = 1,

        /// <summary>
        /// Type reference
        /// Indicated that the type is used (variable and field declaration, attributes, function return type, etc)
        /// </summary>
        TypeReference = 2,

        /// <summary>
        /// Interface implementation
        /// Indicates that the source entity is implementing this interface
        /// </summary>
        InterfaceImplementation = 3,

        /// <summary>
        /// Class Extended
        /// Indicates that the source entity is extending this class or interface
        /// </summary>
        ClassExtended = 4,

        /// <summary>
        /// Test Call
        /// Indicates that the source entity (test) directly or indirectly calls an application method.
        /// </summary>
        TestCall = 5,

        /// <summary>
        /// Property
        /// Indicates that the source entity has a certain property.
        /// </summary>
        Property = 6,

        /// <summary>
        /// Attribute reference
        /// Indicated that an Attribute is used
        /// </summary>
        Attribute = 7,

        /// <summary>
        /// Test Helper Call
        /// Indicates that the source entity is a test helper.
        /// </summary>
        TestHelperCall = 8,

        /// <summary>
        /// Metadata or code Tag reference
        /// Indicates that the source tag is used on a metadata element, class or a method or a line of code.
        /// </summary>
        Tag = 9,

Let’s try doUpdate:


As we can see, result is different to AX 2012, where we could search for an individual table, now all Common methods have reference to Common.


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:


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


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.


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 = '';


    //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>
    // </GEERU>
    InventDim       inventDim;

    if (_forupdate)

    // Fields might not have been selected on the specified buffers, or might have been updated since selection

    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;
        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 <--

    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.

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
tos = mailer.tos();

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
tos = mailer.tos();
/* Orig -->
Orig <-- */
//multiple recipients fix -->
emailAddresses = SysEmailDistributor::splitEmail(outgoingEmailTable.Recipient);
enum = emailAddresses.getEnumerator();
while (enum.moveNext())
//multiple recipients fix <--

By the way this issue is fixed in AX 7.

Cannot create a record in Dimension history for documents (InventReportDimHistory). Error on Vendor invoice posting.

Usually we can get this error for non PO vendor invoice.

To fix it you need to change one of these two number sequences:

  • Internal invoice
  • Internal credit note


Because if they have identical structure they will generate  same numbers and that’s the real cause of this error.

Now let’s look what is happening under the hood.

Unique index of InventReportDimHistory consists of four fields:

  • TransRefId
  • InventTransId
  • TransactionLogType
  • InventDimId

InventTransId is always blank, because non PO invoice lines could have only non-stocked items or procurement categories.

InventDimId could be identical for multiple lines, for example “AllBlank” or any site and warehouse combination.

TransactionLogType is a document type. For invoice it is “PurchInvoice”, for packing slip –  “PurchPackingSlip” and so on.

The last field is TransRefId which is based on InternalInvoiceId field from the Invoice Journal.

InternalInvoiceId is number sequence generated, so it should be unique for each posted invoice.  However, AX could use two different number sequences for this field.

First one is used for invoices and second for credit notes and if this two number sequence have same structure, for example usmf-#######, you could hit this issue when you will create credit note, because at some point AX could generate number for credit note that was already used for invoice.

Please note that other number sequences can be used for some localisations like RU or MY. To get list of all possible number sequence you can look at PurchInvoiceJournalCreate.allocateNumAndVoucher() method.