Friday, January 16, 2015

X++ types Date and UtcDateTime are not implicitly converted to System.DateTime in .NET CIL, in AX 2012

Consider the following code sample:
System.Web.HttpCookie   cookie  = new System.Web.HttpCookie('test');
date                    dateVar = today() + 1;
cookie.set_Expires(dateVar);
It's just a .NET class object with a property of System.DateTime type initialized by a value of X++ date type. This code works perfectly in X++, but in .NET CIL it raises System.NotSupportedException with a message DateTimeConverter cannot convert from Microsoft.Dynamics.Ax.Xpp.AxShared.Date (or DateTimeConverter cannot convert from Microsoft.Dynamics.Ax.Xpp.AxShared.utcdatetime in case of X++ utcDateTime type).
According to MSDN article How to: Marshal Between X++ and CLR Primitive Types [AX 2012]
in Microsoft Dynamics AX, the X++ language does implicit conversion or marshaling between several X++ primitive types and their counterpart types managed by the common language runtime (CLR).
such as dateSystem.DateTime conversion. Well, in fact it does utcDateTimeSystem.DateTime conversion, too! But when X++ code runs in .NET CIL you have to explicitly convert values between date/utcDateTime types and System.DateTime. And actually you should do that anyway ‐ just in case your code happens to run in .NET CIL, otherwise you'll get System.NotSupportedException when you least expect it...

Saturday, September 27, 2014

X++ Operators Div and Mod Behave Differently in .NET CIL, in AX 2012

X++ operators div and mod in CIL can cast their operands differently and return result of different base types compared to X++ interpreter. Consider these examples:
Test operationX++ resultsCIL results
0 div 2Integer0Int640
intMax() div 2Integer1073741823Int641073741823
realMax() div 2Integer1073741823Int644999999999999999
int64Max() div 2Int644611686018427387903Int644611686018427387903
intMax() mod 2Integer1Integer1
realMax() mod 2Integer1Real1,00
int64Max() mod 2Int641Int641

Tested in AX 2012 R2 with kernel builds 6.2.1000.4051 (CU7) and 6.2.1000.8310.

Tuesday, June 10, 2014

AX 2012: don't party with deleted PurchLines

In previous version of Dynamics AX if you delete a PurchLine record - it's gone from the table; it means that for a given purchase order you've got only "active" actual PurchLine records in the database table. AX 2012 has introduced a lot of new and enhanced Procurement and sourcing features including Change management for purchase orders. From a developer's perspective it means that PurchLine records are no longer deleted - instead they are marked with IsDeleted flag and filtered out on regular forms (obviously except for Purchase order versions form). So in customizations it's now vital to mind that and to select or update only PurchLine records with IsDeleted == NoYes::No predicate in the where clause. PurchTable.queryPurchLine() can provide you with a query that takes this into account.

Friday, May 02, 2014

Best Practice checks for objects in the current layer only in AX 2012

It took a small customization to make Best Practice checks in AX 2009 to omit application objects that are not present in the current application layer, which really makes a difference in terms of compilation speed. In AX 2012 you have this feature out of the box! In the development environment navigate to Tools > Options > Development > Best Practices and change the Layer setting to "Skip nodes from lower layers".

Thursday, September 01, 2011

Script to recreate SqlDictionary records in AX 2009

Here's a script that can recreate SqlDictionary records in AX 2009 for any field of (nonsystem) table - in case such record is missing for some reason while the field exists in the database. I used this script during an upgrade to AX 2009 last year. Some fields in the standard application got the same IDs as fields added during customizations and they had different base types, of course, and it seemed impossible to fix the situation using standard means. The script might be of interest to those who wants to know the relations between the SqlDictionary and the AOT.
#macrolib.DictField
// This type is used instead of Types::Int64 for fields of type
// RecId/RefRecId/createdTransactionId/modifiedTransactionId
#define.RecIdBaseType   (49)
// For nonsystem fields of type UtcDateTime an additional field is created
// that holds the actual time zone in which the value has been set
#define.TZIDsuffix      ('_TZID')          

SqlDictionary   sqlDict;
SysdictType     dictType;
DictTable       dictTable;
DictField       dictField;
ArrayIdx        arrIdx;
Counter         numOfSqlFields; // number of records created in SqlDictionary
fieldName       fieldName;
fieldId         fieldId;
tableId         tableId = tablenum(TheTable2Fix);  // TARGET

boolean processTableField(
    DictField   _dictField,
    ArrayIdx    _arrIdx,
    boolean     _isTzIdField = false
    )
{
    ArrayIdx    dictArrIdx;
    str         infoName;       // this field name is for messages only
    FieldName   sqlName;
    boolean     ret;
    ;
    if (_isTzIdField)
    {
        if (    _dictField.baseType()   != Types::UtcDateTime
            ||  _dictField.id()         == fieldnum(Common, createdDateTime)
            ||  _dictField.id()         == fieldnum(Common, modifiedDateTime)
           )
        {
            throw error(Error::wrongUseOfFunction(funcname()));
        }
        dictArrIdx  = _dictField.arraySize() + _arrIdx;
        sqlName     = _dictField.dateTimeTimeZoneRuleFieldName(_arrIdx - 1);
        infoName    = _dictField.name() + #TZIDsuffix;
    }
    else
    {
        dictArrIdx  = _arrIdx;
        sqlName     = _dictField.name(DbBackend::Sql, _arrIdx);
        infoName    = _dictField.name();
    }
    select firstonly sqlDict
        where   sqlDict.tabId   == _dictField.tableid()
            &&  sqlDict.fieldId == _dictField.id()
            &&  sqlDict.array   == dictArrIdx
                ;
    if (!sqlDict)
    {
        sqlDict.clear();
        sqlDict.initValue();
        sqlDict.tabId           = _dictField.tableid();
        sqlDict.fieldId         = _dictField.id();
        sqlDict.array           = dictArrIdx;
        sqlDict.name            = strupr(_dictField.name(DbBackend::Native, _arrIdx));
        sqlDict.sqlName         = sqlName;
        dictType                = new SysDictType(_dictField.typeId());
        if (_isTzIdField)
        {
            sqlDict.fieldType   = Types::Integer;
        }
        else
        if (        _dictField.id()     == fieldnum(Common, RecId)
            ||      _dictField.id()     == fieldnum(Common, createdTransactionId)
            ||      _dictField.id()     == fieldnum(Common, modifiedTransactionId)
            ||      _dictField.typeId() == extendedtypenum(RecId)
            ||      _dictField.typeId() == extendedtypenum(RefRecId)
            ||  (   dictType
                &&  dictType.isExtending(extendedtypenum(RecId))
                )
           )
        {
            // This type is used instead of Types::Int64 for fields of type
            // RecId/RefRecId/createdTransactionId/modifiedTransactionId
            sqlDict.fieldType   = #RecIdBaseType;
        }
        else
        {
            sqlDict.fieldType   = _dictField.baseType();
        }
        sqlDict.strSize         = _dictField.stringLen();
        sqlDict.shadow          = bitTest(_dictField.flags(), #DBF_SHADOW);
        sqlDict.rightJustify    = bitTest(_dictField.flags(), #DBF_RIGHT);
        sqlDict.flags           = sqlDict.shadow;   // not _dictField.flags() at all!
        sqlDict.nullable        =   _dictField.baseType() == Types::Container
                                ||  _dictField.baseType() == Types::VarString
                                    ;
        if (sqlDict.validateWrite())
        {
            sqlDict.insert();
            ret = true;
            info(strfmt(@"Created record for field %1.%2%3",
                        dictTable.name(), infoName,
                        _dictField.arraySize() > 1 ? strfmt(@"[%1]", _arrIdx) : ''));
            // for all nonsystem UtcDateTime fields we also create a related TZID-field
            if (   !_isTzIdField
                &&  _dictField.baseType()   == Types::UtcDateTime
                &&  _dictField.id()         != fieldnum(Common, createdDateTime)
                &&  _dictField.id()         != fieldnum(Common, modifiedDateTime)
               )
            {
                processTableField(_dictField, _arrIdx, true);
            }
        }
        else
        {
            ret = checkFailed(strfmt(@"%1 record for %2.%3 was not created",
                                     tablestr(SqlDictionary), dictTable.name(), infoName));
        }
    }
    return ret;
}
;
dictTable = new DictTable(tableId);
if (!dictTable)
{
    throw error(strfmt(@"Failed to create %1 for '%2' (%3)",
                classstr(DictTable), tableid2name(tableId), tableId));
}
if (dictTable.isSystemTable())
{
    throw error(strfmt(@"'%1' is a system table, no way...", dictTable.name()));
}
if (!dictTable.isSql())
{
    throw error(strfmt(@"Table '%1' should not be in DB", dictTable.name()));
}
for (fieldId = dictTable.fieldNext(0); fieldId; fieldId = dictTable.fieldNext(fieldId))
{
    dictField = dictTable.fieldObject(fieldId);
    if (dictField && dictField.isSql())
    {
        fieldName = dictField.name();
        for (arrIdx = 1; arrIdx <= dictField.arraySize(); arrIdx++)
        {
            numOfSqlFields++;
            processTableField(dictField, arrIdx);
        }
    }
}
select firstonly sqlDict
    where   sqlDict.tabId   == tableId
        &&  sqlDict.fieldId == 0
            ;
if (!sqlDict)
{
    sqlDict.clear();
    sqlDict.initValue();
    sqlDict.tabId       = tableId;
    sqlDict.name        = strupr(dictTable.name());
    sqlDict.sqlName     = dictTable.name(DbBackend::Sql);
    sqlDict.strSize     = numOfSqlFields;       // for the table "header" - num of fields 
    sqlDict.flags       = dictTable.isView();   // that's the way it is
    sqlDict.insert();
    info(strfmt(@"Created record for table %1", dictTable.name()));
}

Wednesday, May 04, 2011

Best Practice checks for objects in the current layer only

There are many Best Practice checks implemented in the standard AX application, which can help you to verify and improve your (or someone’s) source code and other application artifacts. But it should be noted that the execution of Best Practice checks:

  • Dramatically degrades compilation speed;
  • Is pointless for application objects that are not present in the current application layer.

So I decided to make Best Practice checks work only for objects in the current application layer. It turned out to be pretty easy to implement - all you need is add a new enum field to turn on or off this new behavior, for instance, to the SysBPParameters table and customize the SysBPCheck class a little.

private void doTreeNode(TreeNode _treeNode)
{
    TreeNodeTraverser   treeNodeTraverser;
    SysBPCheckBase      sysBPCheckBase;
    TreeNode            treeNodeToRelease;
    boolean             checkChildren = true;
    TreeNodePath        parentPath;
    int                 infologLines;

    Map map = new Map(Types::Integer, Types::Integer);
    MapEnumerator enum;
    int length;
    ;
    treeNodeToRelease = null;
//  if (_treeNode)                              //-gl00mie, 14.02.2011
    if (this.mustCheckTreeNode(_treeNode))      //+gl00mie, 14.02.2011
    {
        treeNodeTraverser = new TreeNodeTraverser(_treeNode);

        while (treeNodeTraverser.next()) // Check Best Practices
        {
            if (!sysCompilerOutput)
            {
                setprefix(treeNodeTraverser.infologPrefix());
            }

            treeNode = treeNodeTraverser.currentNode();
            if(treeNode)
            {
                length=strlen(treeNode.treeNodePath());
            }
            else
            {
                length=0;
            }
            if (treeNodeToRelease && SysTreeNode::isApplObject(treeNode) &&
                (treeNodeToRelease.sysNodeType()!= #NT_DBVIEW || 
                (treeNode && treeNode.sysNodeType()!= #NT_QE) ||
                length < strlen(treeNodeToRelease.treeNodePath()) ||
                substr(treeNode.treeNodePath(),1,strlen(treeNodeToRelease.treeNodePath())) !=
                treeNodeToRelease.treeNodePath()))
            {
                treeNodeToRelease.treeNodeRelease();
                treeNodeToRelease = null;
            }
//          if (checkChildren ||                        //-gl00mie, 14.02.2011
            if ((checkChildren ||                       //+gl00mie, 14.02.2011
                substr(treeNode.treeNodePath(),1,strlen(parentPath)) != parentPath)
                &&  this.mustCheckTreeNode( treeNode )) //+gl00mie, 14.02.2011
            {
                sysBPCheckBase = this.getSysBpCheckBase(treeNode);
                sysBPCheckBase.parmSysBPCheck(this);
                // ...
And here's the new SysBPCheck method that decides whether to run BP checks for a TreeNode:
// gl00mie, 14.02.2011
protected boolean mustCheckTreeNode(TreeNode _treeNode)
{
    UtilEntryLevel  applObjectLayer;
    UtilEntryLevel  currentAOLayer;
    boolean         ret;
    ;
    if (_treeNode)
    {
        ret = true;
        currentAOLayer = currentAOLayer();
        // if you develop on the sys layer then this modification is obviously pointless
        if (currentAOLayer > UtilEntryLevel::sys)
        {
            applObjectLayer = _treeNode.applObjectLayer();
            if (    applObjectLayer < currentAOLayer
                &&  enum2str(_treeNode.applObjectType()) != ''
               )
            {
                // depending on your setup we can skip BP checks for objects situated on lower layers
                // NB! you'll have to add this new field to a table and to a form by yourself
                ret = !SysBPParameters::find().CheckBPForCurrentAOLayerOnly;
            }
        }
    }
    return ret;
}

Saturday, April 30, 2011

Query forceSelectOrder and forceNestedLoop hints are not saved when you pack and unpack a query

In some situations it's useful to take a query from a form or a calling class, transform this query somehow and use in another part of the application. For instance, you might take a query from a form datasource to process all the records queried by the form, or take a query and transform it to count records that can be fetched by the query (what SysQuery::countTotal() does), or just pass a query object from one tier to another (client to server). In all those cases you would do something like this:

Query newQuery = new Query(oldQuery.pack());

Yes, I know that in case of a form datasource query you should also take care of dynalinks but this is not the point. The point is that you might not get the same execution plan for the resulting query because forceSelectOrder and forceNestedLoop hints are not preserved during query pack/unpack. This is true at least for the AX 2009 SP1 RU7 kernel (5.0.1500.4570) so for now if you use these hints you should not rely solely on the kernel when you copy a query. For this purpose you can create a SysQuery method like this:

/// <summary>
/// Copies a query including a one with dynalinks and hints
/// </summary>
/// <param name="_q">
/// source query
/// </param>
/// <param name="_copyHints">
/// must the forceSelectOrder and forceNestedLoop hints also be copied
/// </param>
public client server static Query copy(
    Query   _q,
    boolean _copyHints = true
    )
{
    QueryBuildDataSource    qbdsOld;
    QueryBuildDataSource    qbdsNew;
    QueryBuildDynalink      qbdl;
    QueryBuildRange         qbr;
    Query                   ret;
    str                     sq;
    Counter                 n;
    ;
    if (!(_q && _q.dataSourceCount()))
    {
        throw error(Error::wrongUseOfFunction(funcname()));
    }
    ret = new Query(_q.pack(false));
    if (_copyHints)
    {
        // Query methods forceSelectOrder() and forceNestedLoop() have mandatory parameters
        // so you cannot use these methods as ordinary properties to find out current values
        sq = _q.dataSourceNo(1).toString();
        if (match(@"^SELECT WITH.* SELECT_ORDER[ ,]", sq))
        {
            ret.forceSelectOrder(true);
        }
        if (match(@"^SELECT WITH.* NESTED_LOOP[ ,]", sq))
        {
            ret.forceNestedLoop(true);
        }
    }
    qbdsNew = ret.dataSourceNo(1);
    qbdsOld = _q.dataSourceNo(1);
    for (n = 1; n <= qbdsOld.dynalinkCount(); n++)
    {
        qbdl = qbdsOld.dynalink(n);
        // clear all existing ranges for this field
        while (qbdsNew.findRange(qbdl.field()))
        {
            qbdsNew.clearRange(qbdl.field());
        }
        // set the range value by the current dynalinked cursor field value
        qbr = qbdsNew.addRange(qbdl.field());
        qbr.value(queryValue(qbdl.cursor().(qbdl.dynamicField())));
        qbr.status(RangeStatus::Locked);
    }
    return ret;
}