MSDyn365FO. How-to pass generic types to .Net method

sorcery.png

There is a wonderful blog post about Generic types in X++. And as It says, the easiest way to work with generic types in X++ is to create .Net library and expose non-generic interface, because X++ compiler has lots of limitations.

However, if you really want to have some fun and stick to X++, it’s possible with a help of reflection.

Let’s say we have a C# class that accepts  List<string[]> and we want to invoke it from X++:

public class Class1
{
    public static string myMethod(List<string[]> list)
    {
        string res = string.Empty;

        foreach (var array in list)
        {
            foreach (string s in array)
            {
                res += s;
            }
        }
return res;
    }
}

We cannot simply declare:

var stringList = new System.Collections.Generic.List<String[]>();

Looks like X++ parser cannot handle “<” and “[” combination, as it gives “Invalid token ‘<‘. ” compile error.

Therefore, instead of 2 lines of code in C#:

var stringArrayList = new System.Collections.Generic.List<string[]> { new string[] { "a", "b", "c" } };
string res = Class1.myMethod(stringArrayList);

We need a bit more in X++:

System.String string;

//array we want to put into a list
System.String[] stringArray = new System.String[3]();
stringArray.SetValue('a', 0);
stringArray.SetValue('b', 1);
stringArray.SetValue('c', 2);

//Type[] array for MakeGenericType
System.Type[] typeParams = new  System.Type[1]();
typeParams.SetValue(string.GetType().MakeArrayType(), 0);

//instantiate System.Collections.Generic.List<String[]>
System.Type listType = System.Type::GetType("System.Collections.Generic.List`1");
var constructedListType = listType.MakeGenericType(typeParams);
var stringArrayList = System.Activator::CreateInstance(constructedListType);

//invoke "Add" using reflection because we cannot cast it back to List<String[]>
var  bindingFlags =  BindingFlags::Instance | BindingFlags::Public;
System.Reflection.MethodInfo methodsInfo = constructedListType.GetMethod("Add", bindingFlags);
System.Object[] params = new  System.Object[1]();
params.SetValue(stringArray, 0);
methodsInfo.Invoke(stringArrayList, params);

info(Class1::myMethod(stringArrayList));

X++ is lacking in .Net features and often it’s easier to create intermediate C# class library. However, as you can see, some limitations are caused by X++ compiler that could be overcome using reflection.

One more reason to avoid global variables

v2pos5d.jpg

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

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

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

            //reset cache
            SysGlobalObjectCache::clearAllCaches();

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

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!