Dynamics AX - AIF, Using Asterix (wild card character) in an Criteria Element Find method

When consuming AIF services via c#, a common ask is can we write a query similar to a Like statement in SQL.

Statements like this one should be possible:

(C#)
var criteriaElements = new AXCSICustomer.CriteriaElement[1];
criteriaElements[0] = new AXCSICustomer.CriteriaElement();

criteriaElements[0].DataSourceName = "DirParty";
criteriaElements[0].FieldName = "Name";
criteriaElements[0].Operator = AXCSICustomer.Operator.Equal;
criteriaElements[0].Value1 = "*bob*
";

var queryCritieria = new AXCSICustomer.QueryCriteria()
{
    CriteriaElement = criteriaElements
};

While you can use the '?' wildcard in the value, AX will strip any other special characters (specifically *",.()<>!= characters).  This happens in the class SysQuery::value() method in AX.

One workaround, as found here, is to use the GreaterOrEqual operator.  This essentially turns the query into a "starts with" statement.


(C#)
criteriaElements[0].Operator = AXCSICustomer.Operator.GreaterThanOrEqual;
criteriaElements[0].Value1 = "bob";
//Matches Bobby, Bob, and Bobsledders, but not Kabobs

It is a start, but not ideal.  Well, turns out it isn't that difficult to enable the feature.  After digging into the source code, the culprit method is getQueryValueForCriteriaElement in the class AxdBase in AX.  Here we see a switch statement that handles all of the supported values for Operator (AifCriteriaOperator enum in AX).  Notice the "Like" operator is conveniently missing.

(X++)
private str getQueryValueForCriteriaElement(AifCriteriaElement _criteriaElement, SysDictField _dictField = null)
{
     str     queryValue;
     anytype value1, value2;


     if (_dictField == null)
     {
         value1 = _criteriaElement.getValue1();
     }
     else
     {
         value1 = AifUtil::getTypedValue(_dictField, _criteriaElement.getValue1());
     }

     switch (_criteriaElement.getOperator())
     {
         case AifCriteriaOperator::Equal:
             queryValue = queryValue(value1);
             break;

         case AifCriteriaOperator::NotEqual:
             queryValue = queryNotValue(value1);
             break;

         case AifCriteriaOperator::Range:
             if (_dictField == null)
             {
                 value2 = _criteriaElement.getValue2();
             }
             else
             {
                 value2 = AifUtil::getTypedValue(_dictField, _criteriaElement.getValue2());
             }
             queryValue = queryRange(value1, value2);
             break;

         case AifCriteriaOperator::GreaterOrEqual:
             queryValue = queryRange(value1, SysQuery::valueUnlimited());
             break;

         case AifCriteriaOperator::Greater:
             queryValue = strFmt('>%1', queryValue(value1));
             break;

         case AifCriteriaOperator::LessOrEqual:
             queryValue = queryRange(SysQuery::valueUnlimited(), value1);
             break;

         case AifCriteriaOperator::Less:
             queryValue = strFmt('<%1', queryValue(value1));
             break;
     }

     return queryValue;
}


Now, ideally we would add the missing value to the AifCriteriaOperator enum, and code the following statement (I am truly baffled why Microsoft excluded this):

(X++)
        case AifCriteriaOperator::Like:
             queryValue = SysQuery::valueLike(value1);
             break;


However, in my testing, upon regenerating the AIF service and refreshing the service reference in C#, the new value is not created in the .NET class.  If you try and cheat by casting a number outside of the enumeration's range (I used 200), upon serialization, the type is validated and throws an error, basically stating the number is not part of the enumeration.

 For now, I'm out of time attempting to fix this "correctly", so without further ado, here is the final solution:

(X++)
private str getQueryValueForCriteriaElement(AifCriteriaElement _criteriaElement, SysDictField _dictField = null)
{
     str     queryValue;
     anytype value1, value2;


     if (_dictField == null)
     {
         value1 = _criteriaElement.getValue1();
     }
     else
     {
         value1 = AifUtil::getTypedValue(_dictField, _criteriaElement.getValue1());
     }

     switch (_criteriaElement.getOperator())
     {
         case AifCriteriaOperator::Equal:
             queryValue = queryValue(value1);
             //<coffeestain>
             //Allow like queries via aif
             if(strContains(value1, "*"))
             {
                 queryValue = value1;
             }
             //</coffeestain>

            
             break;

         case AifCriteriaOperator::NotEqual:
             queryValue = queryNotValue(value1);
             break;

         case AifCriteriaOperator::Range:
             if (_dictField == null)
             {
                 value2 = _criteriaElement.getValue2();
             }
             else
             {
                 value2 = AifUtil::getTypedValue(_dictField, _criteriaElement.getValue2());
             }
             queryValue = queryRange(value1, value2);
             break;

         case AifCriteriaOperator::GreaterOrEqual:
             queryValue = queryRange(value1, SysQuery::valueUnlimited());
             break;

         case AifCriteriaOperator::Greater:
             queryValue = strFmt('>%1', queryValue(value1));
             break;

         case AifCriteriaOperator::LessOrEqual:
             queryValue = queryRange(SysQuery::valueUnlimited(), value1);
             break;

         case AifCriteriaOperator::Less:
             queryValue = strFmt('<%1', queryValue(value1));
             break;
         case AifCriteriaOperator::Like:
             queryValue = SysQuery::valueLike(value1);
             break;
            
     }

     return queryValue;
}


Now, as long as the Operation is Equals, the system will honor * wildcards as it should.

If anyone has any information on how .NET retrieves the schema for the above enumeration, I will update this post with a more elegant solution.

Comments

Popular posts from this blog

SQL Reporting Services - Viewer broken in Non-IE Browsers: FIX

Dynamics AX 2012 - Visual Studio Team Services Custom Work Item Fields Break Version Control (Error TF237121)

Dynamics AX SysFileDeployment Framework - Deploy files automatically (using resources) to the client on login