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.

Advertisements

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!

MSDyn365FO. Collation conflict during data upgrade from AX 2012

3-4.jpg

AX 2012 has well-known inconsistency in columns collation that has been reported before and now it is causing issues during data upgrade to latest version (8.0) of Dynamics 365 for Finance and Operations.

Data upgrade fails on step 9. postSync for data upgrade. From RELEASEUPDATESCRIPTSERRORLOG table you can get actual error: “[Microsoft][ODBC Driver 13 for SQL Server][SQL Server]Cannot resolve the collation conflict between  “Latin1_General_CI_AS” and “SQL_Latin1_General_CP1_CI_AS” in the equal to operation.” It fails on the next x++ update statement in ReleaseUpdateDB80_TaxGTE_IN.updateTaxMeasureType() method:

update_recordset crosscompany taxMeasureType
    setting ClassNumber = sysModelElement.AxId
       where !taxMeasureType.ClassNumber
       join AxId from sysModelElement
       	where sysModelElement.ElementType == UtilElementType::Class
           && sysModelElement.Name == taxMeasureType.ClassName;

It was introduced in 8.0 update. Here it uses sysModelElement.Name and taxMeasureType.ClassName fields that have different collation.

Now we have several options to fix this:

  • Skip this method if you don’t use IN localization.
  • Convert source DB to D365 server collation.
  • Change collation of Name column in ModelElement table.

I already knew that we cannot simple use

ALTER DATABASE AxDB COLLATE SQL_Latin1_General_CP1_CI_AS;   <span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

So, I looked at bacpac option proposed by Lane Swenka. We can export DB to a bacpac, edit collation there and import it again. But after I’ve got several thousand errors during the export I decided to switch to other options. I decided not to touch ModelElement because I hope that there is a reason behind its collation, so I went easiest rout for me – skip the code I don’t need.

I did not find nice and easy way to skip this method because everything is done via reflection and it’s hard to skip something using extension, but there is always a dirty way!

Quick extension forces D365 to run different method one more time that updates nothing instead of method I don’t want:

[ExtensionOf(classStr(SysDictClass))]
public final class SysDictClass_FUS_Extension
{
    public static anytype invokeObjectMethod(Object _object, IdentifierName _methodName, boolean _tryBaseClass)
    {
        if (_methodName == "updateTaxMeasureType")
        {
            _methodName = "updateTaxDocumentExtensionIN";
        }

        return next invokeObjectMethod(_object, _methodName, _tryBaseClass);
    }
}

That’s all. After this small hack update goes nice and smoothly.

DontTryTHisAtHome

Solution proposed here is not a real solution, but a hack. It is ok for me because I’m 100% sure that I don’t need this data to be upgraded. I would strongly recommend converting DB to SQL server collation or raise this with MS in case you hit this to get real solution that would be supported.

Happy hacking!

MSDyn365FO. How to playback a task recording from X++.

s-l300

Warmup of a VM is an old issue and everyone tries to find a workaround. It is critical for demo VMs that are turned off most of the time and you don’t want to demo a product that is slow as hell.

Well-known workaround is to manually open all the forms you are going to use but it’s boring and time-consuming task. One of the yammer users came up with an idea to use task recording and here is an implementation in its dirtiest way (calling private methods on object without any validation) that is good enough for demo anyway, so feel free to improve this code:

class WarmUpRunnableClass
{
    public static void main(Args _args)
    {
        var currentRecording = SysBPMHelper::LoadLocalizedRecordingFromLCS(80244103); //BpmLineId here.

        Args args = new Args();
        args.name(formstr(SysTaskRecorderPane));

        FormRun formRun = ClassFactory.formRunClass(args);
        formRun.init();
        formRun.run();
        formRun.loadRecording(currentRecording);
        formRun.maintainExistingRecording();
        formRun.playToStep(100000);
        formRun.wait();
    }
}

This runnable class could be executed from URL, so you can add it to Windows Task Scheduler and execute it on the VM start automatically.

80244103 is a task recording id that is hardcoded here for simplicity. Previously I recorded it and saved to BPM. You can improve this code and add some smarties to grab it from BPM using search by keyword or any other logic you like.

For those who think that hardcode is good enough to get this number you need to go to LCS, open BPM and find task recording you want to run. PLID value from URL is a number you need:

PLID.png

 

MSDyn365FO. How to switch from IIS Express to IIS on development VM.

All new environments starting from PU12 are using IIS Express in debug mode and often due to random issues we have to restart it using “Restart IIS Express” option in the Dynamics 365 menu. There are multiple discussions on community and yammer forums regarding these issues and Joris de Gruyter mentioned that it is possible to switch back to IIS if you have admin access (environment deployed to customer’s or partner’s own Microsoft Azure subscription), but did not explain how 🙂

All we need to do is to edit DynamicsDevConfig.xml file that could be found in ‪K:\AosService\PackagesLocalDirectory\Bin folder (drive letter could be different for your VM). Change RuntimeHostType value from IISExpress to IIS and that’s all.

DynamicsDevConfig

You may find same file under %userprofile%\Documents\Visual Studio 2015\Settings\ , however, Visual Studio rolls back any changes to RuntimeHostType if value is different to value in \AosService\PackagesLocalDirectory\Bin\DynamicsDevConfig file, so don’t try to change it for individual user.

D365FO. “The authentication process was not successful. Please contact your system administrator.” error during Connect to Lifecycle Services setup.

As we know, there are hundreds of task recordings available out of the box in LCS library. Before first use, system administrator should authorize D365FO to access LCS and often it is not possible due to the old bug. When system administrator goes to Settings -> Task recorder -> Play recording as guide -> Open from Lifecycle Services and clicks “Select the Lifecycle Services library” “Connect to Lifecycle Services” dialog pops up.

Connect to Lifecycle Services

There is a link “Click here to connect to Lifecycle Services” and when administrator clicks on it he may receive “The authentication process was not successful. Please contact your system administrator.” error.

It’s is caused by empty value of “LCS.GettingStartedLibrary” parameter in web.config file.

To fix it go to K:\AosService\WebRoot and edit web.config file. Find LCS.GettingStartedLibrary tag and set value to any integer that is greater than 0, save file and restart IIS. Don’t forget to backup web.config before doing any changes.

WebConfigLCSGettingStarted

Probably LCS should put BPM library id there, however, it works with any random positive integer as well. To find BMP project id, go to LCS, open your project and click on “Business process modeler” tile. Then select library that contains task recording.  In the URL of the page grab value of FWK parameter

LCSBPMURL.png

Hopefully, this bug will be fixed soon and we won’t have to play with web.config file anymore.

D365FOE. How to override form data source field lookup method.

A long time ago, I wrote a blog post explaining how to override form data source field methods. I skipped lookup, most popular method, because it did not work and logged a bug with MS hoping that it would be fixed soon. It took more time than I expected, but finally, it works!

Here is a small example how to override it. I’m going to use approach similar to previous post, full example is available on GitHub. Version I’m using is 7.3 PU13.

/// <summary>
/// Handles events raised by <c>SalesTable</c> form.
/// </summary>
public class SalesTableEventHandler
{
    /// <summary>
    /// Post event handler for <c>SalesTable</c> <c>SalesLine</c> Initialized event.
    /// </summary>
    /// <param name=“_sender”></param>
    /// <param name=“_e”></param>
    [FormDataSourceEventHandler(formDataSourceStr(SalesTable, SalesLine), FormDataSourceEventType::Initialized)]
    public static void SalesLine_OnInitialized(FormDataSource _sender, FormDataSourceEventArgs _e)
    {
        var overrides = SalesTableFormExtensionOverrides::construct();

        _sender.object(fieldNum(SalesLine, ItemId)).registerOverrideMethod(methodStr(FormDataObject, lookup),
            methodStr(SalesTableFormExtensionOverrides, ItemId_OnLookup), overrides);
    }
}

/// <summary>
/// Contains methods which are used to override <c>SalesLine</c> data source field methods.
/// </summary>
public class SalesTableFormExtensionOverrides
{
    protected void new()
    {
    }

    /// <summary>
    /// Constructs a new instance of <c>SalesTableFormExtensionOverrides</c> class.
    /// </summary>
    /// <returns>
    /// A <c>SalesTableFormExtensionOverrides</c> class.
    /// </returns>
    public static SalesTableFormExtensionOverrides construct()
    {
        return new SalesTableFormExtensionOverrides();
    }

    /// <summary>
    /// Provides a lookup for the <c>InventTable</c> table
    /// </summary>
    /// <param name = "_callingControl">
    /// The form string control object with which to perform the lookup.
    /// </param>
    public void ItemId_OnLookup(FormStringControl _callingControl)
    {
        SysTableLookup sysTableLookup = SysTableLookup::newParameters(tableNum(InventTable), _callingControl);

        sysTableLookup.addLookupfield(fieldNum(InventTable, ItemId));
        sysTableLookup.addLookupfield(fieldNum(InventTable, NameAlias));

        sysTableLookup.performFormLookup();
    }
}

As you may notice, signature of this method is different to signature of other methods, like validate or jumpRef. They accept FormDataObject as a parameter and lookup() accepts FormStringControl. It looks a bit inconsistent for me and we need to be extra careful here because you can register override method with any signature and get error in the run-time.