X++ to CIL: Object must implement IConvertible

Somebody asked in Dynamics User Group forum about an error thrown by RetailCommonWebAPI running in a batch in AX 2012 (RetailCommonWebAPI in batch mode). I don’t want to discuss that particular problem here, but I want to show the underlying cause, because it’s actually quite tricky.

What I’m discussing below is related to CIL generated from X++, therefore if you want to try the sample code, make sure that you run it in CIL. It doesn’t require running in batch.

Let’s start with a simple piece of X++ code. It creates a Hashtable (using .NET Interop) and passes it to the constructor of another hash table. (I don’t bother adding any data to the hash table, because it’s not necessary for the demonstration.) This code works without any problem.

System.Collections.Hashtable t1 = new System.Collections.Hashtable();
System.Collections.Hashtable t2 = new System.Collections.Hashtable(t1);

Now let’s change a single thing – the type of the first variable will be defined as CLRObject instead of Hashtable. Note that the content of the variable is exactly the same as before.

CLRObject t1 = new System.Collections.Hashtable();
System.Collections.Hashtable t2 = new System.Collections.Hashtable(t1);

If you run this code (in CIL), you’ll get an error:

System.InvalidCastException: Object must implement IConvertible.
   at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)

It’s an interesting result. I would expect the call to fail, because there is no Hashtable’s constructor accepting an object (System.Object), but it’s not the reason. It’s referring to a call to Convert.ChangeType(), but where did it come from?

The answer can’t be found in X++; you have to look at the generated CIL code. This is what I got from the first snippet (after decompiling CIL to C#):

t2 = new Hashtable((IDictionary) t1);

Hashtable has two constructors accepting a single parameter: one takes capacity (int) and one takes elements in the form of a dictionary (such as a hash table). We want to use the latter and that’s exactly what happens.

Now look at CIL generated for the other snippet:

t2 = new Hashtable((int) Convert.ChangeType(t1, typeof(int)));

This is unexpected and incorrect code. The generator of CIL from X++ somehow decided to use the constructor accepting int and it’s trying to convert t1 to int. It must fail, because there is no conversion from Hashtable to int. And even if it succeeded, we would get an empty Hashtable (which is not what we would want, if t1 contained some values).

If the CIL generator doesn’t know the underlying type, it shouldn’t try to guess it, because it’s likely to fail and we end up with weird and unpredictable errors. A compile-time failure would be a much safer approach.

Just for completeness, this is how CIL looks like if you declare the variable as System.Collections.IDictionary (as I suggested in the discussion forum):

t2 = new Hashtable(t1);

Because the type is exactly matching the expected type, there is neither type casting nor any conversion.

Summarized values in AX form

Users sometimes want to see both individual transactions in a grid of a Dynamics AX form, and some summarized values, such as the total amount or the number of lines (often above or below the grid). Iterating through the whole datasource and getting values one by one isn’t efficient, especially if the query returns many rows. A much better solution is taking the query, modifying it to use an aggregation function (such as SUM() or COUNT()) and sending a single, efficient request to database.

My example assumes that I have a form showing customer invoice lines and I want to calculate the total amount of all lines fulfilling current filters (and show it in a separate control).

public void updateTotal()
{
    // Copy the query
    Query query = new Query(CustInvoiceTrans_ds.queryRun().query());
 
    QueryBuildDataSource qbds = query.dataSourceTable(tableNum(CustInvoiceTrans));    
    QueryRun qr;
    CustInvoiceTrans summedTrans;
 
    // Sum LineAmountMst
    qbds.addSelectionField(fieldNum(CustInvoiceTrans, LineAmountMst), SelectionField::Sum);
 
    qr = new QueryRun(query);
    // Run the query
    qr.next();
 
    // Get the data
    summedTrans = qr.get(tableNum(CustInvoiceTrans));
 
    // Set the new sum to the control
    Total.realValue(summedTrans.LineAmountMST);
}

The first statement is extremely important, because it defines which query you want to use. I take CustInvoiceTrans_ds.queryRun().query(), because I want to respect filters defined by users. If it wasn’t the case, I would use CustInvoiceTrans_ds.query(). Both scenarios are valid; the choice depends on your functional requirements.

It’s also worth noting that I modified a copy of the query. If I modified the query used by the datasource, I would actually get the summed result in my grid, which wouldn’t make sense.

Then I just have to call the method every time when the datasource query executes.

public void executeQuery()
{
    super();
    element.updateTotal();
}

Refreshing form parts

When using form parts in AX 2012, you sometimes need to explicitly refresh their data based on an event in the main form. It may not be completely obvious how to do it, but it’s not too complicated in the end.

Form parts are actually forms by themselves and if you know how to manipulate forms at runtime, you know how to work with parts too. The tricky part it getting a reference to a form part.

One of possible solutions is adding the following method to SysSetupFormRun class (so it’s available to all forms):

public FormRun getFormPartByName(str _name)
{
    PartList partList = new PartList(this);
    FormRun part;
    int i;
 
    for(i = 1; i <= partList.partCount(); i++)
    {
        part = partList.getPartById(i);
 
        if (part.name() == _name)
        {
            return part;
        }
    }
    return null;
}

As you see, it iterates all parts in the form and finds a part with the given name.

Then you can call it from your form to get a reference to a particular part and do anything you like with it, such as refreshing the data:

public void refreshMyFactBox()
{
    SysSetupFormRun formRun = this as SysSetupFormRun;
    FormRun factBox = formRun.getFormPartByName('MyInfoPart'));
 
    if (factBox)
    {
        factBox.dataSource().research();
    }
}

Note that if it’s a form part, you have to provide the name of the underlying form, such as:

FormRun factBox = formRun.getFormPartByName(formStr(MyFormPartForm));

Configuration of LCS System Diagnostics

System Diagnostics from Dynamics Lifecycle Services is a really handy tool – it collects data about your Dynamics AX environments and warns you if your setup is not optimal from performance perspective, if a number sequence is running out of available numbers, if batches are failing and so on. It allows you to act proactively, rather than waiting for something serious to happen.

The only problem with this tool is configuration, because you have to grant the service account permissions to quite a few things, but you typically don’t want to allow everything. The recommended configuration therefore cherry-picks individual items to set permissions for, such as individual registry keys. It’s well-documented, unfortunately it still consists of a large number of manual steps and it’s very easy to do something wrong, especially if you have many servers to configure.

Below you can find scripts automating a few tasks, such as adding the service account to necessary user groups. It’s by no means exhaustive and you’ll still have to do many things manually, but it’s better than nothing. I didn’t mean it as any ambitious project; I merely implemented a few easy wins last time when I was configuring System Diagnostics – and now I’m sharing it with you.

Examples below expect that you’ve set variables with the domain and service account name:

$domain = 'MyDomain'
$accountName = 'LcsServiceAccount'

You’ll likely need to run the scripts “As administrator”.

# Adds system diagnostics service account to AX
Function Add-LcsAccountToAX
{
    Param(
        [Parameter(Mandatory=$True)]
        [string]$User,
        [Parameter(Mandatory=$True)]
        [string]$Domain,
        [string]$AxUserId = 'LcsDiag'
    )
 
    # Requires AX management module (e.g. running in AX management shell)
    New-AXUser -UserName $AccountName -UserDomain $Domain -AXUserId $AxUserId -AccountType WindowsUser
    Add-AXSecurityRoleMember -AxUserID $AxUserId -AOTName SysBusinessConnectorRole
}
 
# Usage:
Add-LcsAccountToAX -User $accountName -Domain $domain
# Grants access to registry keys
Set-RegistryReadPemissions
{
    Param(
        [Parameter(Mandatory=$True)]
        [string]$Account,
        [Parameter(Mandatory=$True)]
        [string]$RegKey
    )
 
    $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($Account,'ReadKey','ObjectInherit,ContainerInherit','None','Allow')
 
    $acl = Get-Acl $RegKey
    $acl.SetAccessRule($rule)
    $acl | Set-Acl
}
 
# Usage:
 
$domainAccount = "$domain\$accountName"
 
# Run on AOS server
Set-RegistryReadPemissions -Account $domainAccount -RegKey 'HKLM:\System\CurrentControlSet\services\Dynamics Server\6.0'
 
# Run on database server
Set-RegistryReadPemissions -Account $domainAccount -RegKey 'HKLM:\System\CurrentControlSet\Control\PriorityControl'
# Adds service account to Windows user groups
Function Add-DomainUserToLocalGroup
{
    Param(
        [Parameter(Mandatory=$True)]
        [string[]]$Group,
        [Parameter(Mandatory=$True)]
        [string]$User,
        [Parameter(Mandatory=$True)]
        [string]$Domain,
        [string]$Computer = $Env:ComputerName
    )
 
    foreach ($g in $Group)
    {
        $adsi = [ADSI]"WinNT://$computer/$g,group"
        $adsi.psbase.Invoke("Add",([ADSI]"WinNT://$domain/$user").path)
    }
}
 
# Usage:
$groups = 'Event Log Readers','Distributed COM Users','Performance Monitor Users' 
Add-DomainUserToLocalGroup -Group $groups -Domain $domain -User $accountName

Preview of Release Management

When you develop a new solution, it’s useless until you deploy it to a production environment where users can actually use it. Before you can do it, you typically need a few extra deployments to test environments. It takes time that people could use for working on new features and it often require many manual steps, which is always error-prone. Automation can both save resources and prevent people from doing something wrong. The need is getting more urgent as release cycle shorten to deliver new features to users as soon as possible.

Release Management for Visual Studio can help you to do that. It allows you to define the release process and approvers, execute deployments, track their history, send email notifications and so on. You still have to define what steps will run and what they’ll do; the value of Release Management is in handling all the infrastructure around.

I spent some time preparing releases of Dynamics AX with Release Management for Visual Studio 2013 and 2015. Unlike Kenny Saelen in his blog series, I used so-called vNext release templates, which is a more lightweight approach, doesn’t require deployment agents and it uses either Powershell or Chef to handle all release tasks (here is documentation, if you’re interested).

I was a bit disappointed, because it lacked quite a few features I needed, especially VSO support for on-premise environments. Also diagnostics of release tasks was quite difficult and a few other things just didn’t behave as I would like.

Therefore I was really keen to jump into a preview version of the new Release Management, which promised to address several of my problems, and I’m happy to announce that it does. It doesn’t do everything I would like (at least not yet), but it’s a great improvement.

Let me demonstrate the basics on a simplified release of Dynamics AX. The project uses Visual Studio Online as the version control and XAML builds running on an on-premise server.

You don’t need your own Release Management server for on-premise deployments anymore; it’s all hosted in Visual Studio Online. You can find it on the Release tab, but currently (October 2015) only if you’re participating in the preview program.

Toolbar

When you create a new release template, you have to define one or more environments. Then you can manage and track how far you’ve released your code, such us that it’s currently in the FAT environment.

Environments

You need some tasks to do the actual work. As you can see, you have quite a few tasks to choose from:

AddTasks

Nevertheless my AX release is implemented completely in Powershell (I just slightly modified my existing scripts, based on DynamicsAxCommunity module), therefore I don’t use any other type of task:

To get files to deploy, link the release definition with an artifact source. In my case, I linked it with a team build, therefore my release will get all files produced by a particular build.

LinkArtifacts

Here I ran into a limitation, because my release scripts are not in the same team project as my AX code and I can’t easily link artifacts from other projects. That’s why I’ve put scripts to my environment and refer to them through absolute paths (\\Server1\RMScripts\…) accessible from all machines. If I could link artifacts from two projects, Release Management would download release scripts for me.

When triggering a new release, I can choose which build contains application files to deploy. I could also configure it to run automatically after a build (such as deploying a test environment immediately after a night build of latest code).

NewRelease

You can track progress of your release directly in your browser, including output from your tasks (such Install-AXModel and axbuild in the picture below). It’s wise keeping tasks relatively small, so you can easily see what’s running (or what failed, in a worse case).

RunningRelease

You need an agent installed in your environment, to handle all communication with VSO and execute tasks. Tasks can connect to other machines and deploy things there, therefore you may have a single agent deploying to dozen servers. On the other hand, you may want more agents, to running multiple releases in parallel or having agents with different capabilities (such as different software installed).

The agent can run as a Windows service, but you can also start it as a normal command-line application and monitor what it does.

Agent

And of course, you can see history of all releases, their status, logs and so on.

ListOfReleases

Although it’s still in a preview stage, it already runs really well. And because it’s so lightweight and allow integration of Powershell scripts with just a few clicks, I think I’ll use it even for small things that didn’t require any sophisticated solution before, but that will benefit from being easily available to the whole team and from archiving history of all runs.

The new Release Management is expected to be released this year; unfortunately I don’t know anything for specific in the moment. In the meantime, you can learn more about it from documentation on MSDN.