Quantcast
Channel: dynamics ax – Goshoom.NET Dev Blog
Viewing all 117 articles
Browse latest View live

Deploying AX 2012 R3 on Azure

$
0
0

The R3 version of Dynamics AX 2012 is finally here and one of its keenly expected features is the deployment on Windows Azure. I haven’t got chance to try it yet, nevertheless I already want to share an idea about how to do it.

You’ll obviously need a Windows Azure subscription. You’ll also need Microsoft Dynamics Lifecycle Services (LCS) where you’ll create a new project (with Product version = Dynamics AX 2012 R3) and link it with the Azure subscription. Then you’ll create a new cloud environment in LCS and deploy it.

More details are available in Deploy Microsoft Dynamics AX 2012 R3 on Azure using Lifecycle Services.


Access denied: SysOperationServiceController

$
0
0

I unexpectedly got Access denied: SysOperationServiceController when trying to call a method through the SysOperation framework in Dynamics AX 2012. It was just a piece of research code and I didn’t want to bother with my own controller, therefore I created and executed a menu item with the following properties:

Property Value
ObjectType Class
Object SysOperationServiceController
EnumTypeParameter SysOperationExecutionMode
EnumParameter Synchronous

The error message is a bit misleading, so it took me a minute to realize that I forget to define which method to call. Setting the Parameters property to MyClass.MyMethod did the job.

(In)valid conversion

$
0
0

X++ compiler is very benevolent when dealing with types – much more than it should be. It happily accepts code that can’t succeed at run-time, as I’ll will demonstrate in a minute. It’s unfortunate, because some errors could have been detected at compile-time and they aren’t. And that’s not all – the X++ runtime is also more benevolent than CLR (Common Language Runtime), therefore the same code may run successfully as native AX and fail in CLR (after compiling X++ to CIL).

Imagine two X++ classes – a base class and a child class.

class BaseClass
{
    public str name() { return "Base"; }
}
 
class ChildClass extends BaseClass
{
    public str name() { return "Child"; }
    void childOperation() {}
}

You can safely assign an instance of the child class to the parent class variable:

BaseClass base = new ChildClass();

This can’t ever go wrong – ChildClass extends BaseClass and it definitely has all its operations, because it inherits them. If you call base.name(), it will execute the implementation in ChildClass, therefore it will return “Child”. This is called polymorphism and it’s a very important component of object-oriented programming.

Now what if you try to do the opposite – to assign a variable of a general type to a specialized one? The C# compiler, for example, would throw an error (“Cannot implicitly convert ‘BaseClass’ to ‘ChildClass’”). The assignment will work only if base variable actually contains an instance of ChildClass. If it contains BaseClass or some other class extending from it, there could be a missing methods or other behavior, such as in the following example:

BaseClass base = new BaseClass();
ChildClass child = base;
child.childOperation();

The child variable here contains an instance of BaseClass, which doesn’t have any childOperation(), therefore the code must fail. That’s why compilers usually don’t allow such code.

Nevertheless this is not the case of X++ compiler – it compiles the code above regardless that it can’t ever work. It will always throw this error: “BaseClass object does not have method ‘childOperation’”.

I have one more extreme example for you:

Object o = new Dialog();
TextIo textIo = o;

You see that we work here with completely unrelated types, but the code is still compilable and runs. The compiler shows the crazy truth – you have a variable of TextIo type containing a Dialog.

Debugger

Don’t believe that an assignment is correct just because the compiler allows it – check types by yourself. In AX 2012, you have is and as operators for this purpose, therefore a better implementation could look like this:

if (base is ChildClass)
{
    child = base;
}

Add a pinch of CIL…

There is one more point to make. If you compile X++ code to CIL, it will run in a different runtime environment (CLR) which is stricter than AX. In our sample code, we saw that AX accepts the assignment on line two and throw an error when calling an non-existing method on line 3:

BaseClass base = new BaseClass();
ChildClass child = base;
child.childOperation();

But if you compile this to CIL and run it in CLR, the assignment itself will fail, because the types are not compatible. You’ll get this error: System.InvalidCastException: Unable to cast object of type ‘Dynamics.Ax.Application.BaseClass’ to type ‘Dynamics.Ax.Application.ChildClass’.

It’s not merely about when you get an error – the following code actually works in X++ but it fails in CIL:

BaseClass base = new BaseClass();
ChildClass child = base;
child.name();

The type of child variable is ChildClass and the compiler allows you to call method of ChildClass. In X++, the assignment (child = base) succeeds and child actually contains an instance of a different type (BaseClass in this case). AX still tries to call the method and if the actual object has a method of the same signature, it gets called. In our example, child.name() returns “Base” – it works, but  the behavior is wrong.

Although the code runs in AX, the assignment is still invalid from CLR perspective and the execution of CIL will fail.

Conclusion

Fortunately, all these things shouldn’t cause you too many troubles if you write your code reasonably. But you may sometimes have incorrect assumptions about the type returned by a method and you get an unexpected type assigned to your variable. Because it doesn’t get detected at this point, both you and the compiler keep the false assumption and you later don’t have a clue why the runtime complains about a missing method. I’ve seen several people struggling with this problem when working with .NET Interop from X++. And the difference between AX runtime and CLR adds some additional complexity.

Instrumentation and tracing

$
0
0

Regardless of how hard we try to write flawless software, sooner or later something goes wrong and we must identify where exactly the problem lies (to be able to fix it). One option is using the debugger, but it’s not always possible – we can’t reproduce the problem, for example. Another way is inserting some trace messages into code (= instrumenting the code) and logging them somewhere – it can reveal what code was called, values of certain variables, identity of a user executing some method and so on.

There are quite a few things you may want to do with such trace messages – filter them by priority or origin, save them to various destinations (e.g. text/XML/EventLog), activate/deactivate tracing without rebuilding your application and so on.

The good news is that .NET already contains such a framework and we can use it.

TraceSource in .NET (4.0)

Instead of jumping into details, let’s look at a rather simple example. If you find it interesting, you can learn more in Tracing and Instrumenting Applications.

We’ll need two main components:

  • A trace source. We’ll pass information about interesting events to the source, but the source itself doesn’t know how (or whether) messages will be logged.
  • Trace listeners. Listeners get information from a trace source and log them somewhere.

In your source code, you simply create a named TraceSource instance and use one of its methods to record an event:

System.Diagnostics.TraceSource ts = new System.Diagnostics.TraceSource("Goshoom.DemoApplication");
ts.TraceInformation("Started");

Because we don’t want to create a new TraceSource every time, it’s typically stored in a static field.

Even if we write something into a trace source, nothing is going to happen unless there is a listener. Listeners can be registered in code as well, but the usual approach is using configuration files, because it’s much more flexible. Check out the following fragment of a configuration file:

<system.diagnostics>
  <sources>
    <source name="Goshoom.DemoApplication" switchValue="Information">
      <listeners>
        <add name="TextFile" 
             type="System.Diagnostics.TextWriterTraceListener" 
             initializeData="DemoApplication.log" />
      </listeners>
    </source>
  </sources>
</system.diagnostics>

We identify the trace source by its name (that’s the value we passed to TraceSource’s constructor) and set a switch saying that we’re interested in messages of severity “Information” and higher. It gives you great flexibility – you can, say, log only errors in production environments and switch to more details only when needed and only for the trace source related to the failing component.

Then we add one listener which writes messages received from the trace source to a text file. You can have several listeners for a single source, listeners shared between sources etc. See Trace Listeners on MSDN to learn a bit more about various types of listeners.

Dynamics AX 2012

If you create .NET libraries to be used by AX as part of an AX process itself, the approach is exactly the same – you’ll create and use a trace source in your library and configure listeners in Ax32.exe.config (if the library is used by AX client) or Ax32Serv.exe.config (for AOS). Naturally, you can also listen to events recorded by existing libraries, such as WCF. (It sometimes helps me a lot, just note that you can’t configure diagnostics in endpoint configuration, you have to do that in AOS configuration).

Furthermore, you can use exactly the same approach in X++ via .NET Interop – our example mentioned before as C# is also valid X++ code:

System.Diagnostics.TraceSource ts = new System.Diagnostics.TraceSource("Goshoom.DemoApplication");
ts.TraceInformation("Started");

If X++ code doesn’t run in CIL, there is a performance penalty when invoking calls via .NET Interop. It’s nothing you have to care about if you do few calls, but it may cause troubles if it’s used in huge loops, for instance.

Infolog

Infolog messages created in AX are also automatically passed to a trace source and you can use usual techniques to log them. If you look at the MSDN article about propagating infolog messages to Event Log, you will recognize the pattern described before:

<configuration>
  <system.diagnostics>
    <trace autoflush="true"/>
    <sources>
      <source name="Microsoft.Dynamics.Kernel.Client.DiagnosticLog-Infolog"
              switchValue="Information">
        <listeners>
          <add name="EventLog" 
               type="System.Diagnostics.EventLogTraceListener" 
               initializeData="Dynamics Infolog"/>
          <add name="TextFile" 
               type="System.Diagnostics.TextWriterTraceListener" 
               initializeData="DynamicsInfologTrace.log" 
               traceOutputOptions="DateTime"/>
        </listeners>
      </source>
    </sources>
  </system.diagnostics>
</configuration>

We have two listeners there (writing messages to Event Log and to a text file) for messages received from Microsoft.Dynamics.Kernel.Client.DiagnosticLog-Infolog trace source.

Let’s try another type of listener – XmlWriterTraceListener:

<add initializeData="infolog.svclog" type="System.Diagnostics.XmlWriterTraceListener"
    name="xmlListener" traceOutputOptions="DateTime, Callstack" />

You can open such files in any text or XML editor, but there is one more option – Service Trace Viewer. I intentionally used .svclog extension instead of .xml, because .svclog files are associated with Service Trace Viewer. If you don’t have it installed, you can get it as part of Windows SDK. See how it looks like:

Service Viewer - Infolog

It has a lot of great features regarding visualization, filtering, opening several trace files in the same time (e.g. client and AOS or multiple AOSes) and so on. You may not appreciate that too much when working with infolog messages, but it’s extremely useful when things start to be a bit more complicated.

I selected Callstack as one of output options – let’s look at what we get from AX:

at System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
at System.Environment.get_StackTrace()
at System.Diagnostics.TraceEventCache.get_Callstack()
at System.Diagnostics.XmlWriterTraceListener.WriteFooter(TraceEventCache eventCache)
at System.Diagnostics.XmlWriterTraceListener.TraceEvent(TraceEventCache eventCache, String source, TraceEventType eventType, Int32 id, String message)
at System.Diagnostics.TraceSource.TraceEvent(TraceEventType eventType, Int32 id, String message)
at Microsoft.Dynamics.Kernel.Client.DiagnosticLog.AddInfologMessage(TraceEventType eventType, Int32 errorType, String message)
at Microsoft.Dynamics.Kernel.Client.DiagnosticLogWrapper.AddInfologMessage(DiagnosticLogWrapper* , Int32 errorType, Char* message)
at System.Windows.Forms.UnsafeNativeMethods.CallWindowProc(IntPtr wndProc, IntPtr hWnd, Int32 msg, IntPtr wParam, IntPtr lParam)
at System.Windows.Forms.NativeWindow.DefWndProc(Message& m)
at Microsoft.Dynamics.Kernel.Client.NativeWindowWithEvents.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at _wWinMainCRTStartup()

If you expected an X++ call stack, this must be a huge disappointment. All what we get is a sequence of .NET architecture calls that as some point call X++ (CallWindowProc()). Although it makes a good sense, it renders the log much less useful than it usually would be. Normally we want to know where exactly an event occured.

We can’t change the call stack generated by .NET – it’s even correct, it’s just not what we want. But we can make a workaround – we can create an additional trace message containing the X++ call stack. We’ll make following changes to the Info class:

classDeclaration
{
    System.Diagnostics.TraceSource ts;
    …
}
 
new
{super();
    ts = new System.Diagnostics.TraceSource("Microsoft.Dynamics.Kernel.Client.DiagnosticLog-Infolog");
    …
}
 
private void addTraceCallstack(str _txt)
{
    str msg = _txt + " - Callstack";
    container c = xSession::xppCallStack();
    str path;
    int i;
    boolean found;
    #AOT
 
    for (i = 1; i <= conLen(c); i += 2)
    {
        path = conPeek(c, i);
        path = substr(path, strfind(path,'\\',1,strlen(path)), strlen(path));
 
        if (found)
        {
            msg += strFmt("\n%1 [%2]", conPeek(c, i), conPeek(c, i+1));
        }
        else
        {
            if (path == #ClassesPath+'\\'+classstr(Global)+'\\'+staticmethodstr(Global, info) ||
                path == #ClassesPath+'\\'+classstr(Global)+'\\'+staticmethodstr(Global, error)||
                path == #ClassesPath+'\\'+classstr(Global)+'\\'+staticmethodstr(Global, warning) ||
                path == #ClassesPath+'\\'+classstr(Global)+'\\'+staticmethodstr(Global, checkFailed))
            {
                found = true;
            }
        }
    }
 
    ts.TraceEvent(System.Diagnostics.TraceEventType::Verbose, 0, msg);
}
 
Exception add()
{
    Exception ex;
    …
    //return super(_exception, (buildprefix?getprefix():'')+_txt);
    ex = super(_exception, (buildprefix?getprefix():'')+_txt);
    this.addTraceCallstack(_txt);
    return ex;
}

We’ve created a new TraceSource instance with the same name as used for standard infologs, which is completely valid. In addTraceCallstack(), we get X++ callstack, drop calls related to infolog handling, format the message and log it as a trace event of type Verbose. This method is triggered by add(), after processing the infolog message itself.

Any infolog message logged through the tracing infrastructure will be followed by another massage with X++ call stack.

Infolog - Callstack You’ll have to set switchValue in configuration file to Verbose. You could also log the call stack with the same severity as the original message – it’s all up to you.

If you don’t have enough, you can also check some new features for tracing in .NET 4.5.

Index fragmentation on xRef tables

$
0
0

Building cross-references for code in Dynamics AX creates millions of records in xRef tables. It has also very negative impact on fragmentation of database indexes.

This is an example of indexes on xRefReferences table from a customer’s environment after rebuilding all cross references.

Before

And here the same set of indexes a few minutes later after a rebuilding indexes:

After

If an index is heavily fragmented, it’s much slower to scan it and you’ll notice performance degradation of all queries using the index. It’s not unusual that opening Add-ins in the context menu in AOT takes several seconds just because of a simple check to xRef tables (to see whether cross-references exist for the given object). And because the index can’t be used effectively, of course.

It’s should be obvious that having right indexes isn’t sufficient – they also have to be in a good shape.

It’s wise to have a scheduled job to maintain fragmentation and statistics of all indexes, nevertheless you may also want to rebuild indexes for xRef tables immediately after updating cross references.

Issue Search

$
0
0

Issue Search is obviously not the coolest part of Microsoft Dynamics Lifecycle Services, but it’s very useful and I would like to show you a few tricks.

If you’ve never heard about Issues Search before, it’s a relatively simple web page where you can find hot fixes released for AX 2012 and even issues currently being investigated. This is how the main page looks like:SearchResults

You can see that there is a text box for search input, some filters and results. Notice that you can filter by version of AX (you sometimes need to open Advanced search and adjust this setting to get what you want), that different states of issues have different colors and that you can directly see KB number and the release date (if applicable).

If you click on an issue, you’ll see a bit more details and most importantly you’ll be able to download released hotfixes.

IssueDetails

Often you can even see changes done by Microsoft in X++ code, which allows you to quickly review the solution and impact on your application (by using View changes or clicking on a particular affected object).

CodeChanges

If you’re familiar with Team Foundation Server, you probably know that this web-based code comparison is a standard feature, so it’s not something unique to Dynamics Lifecycle Services. But it’s unusual to see such a level of transparency from software vendors and I must appreciate that Microsoft decided to share all these details with us.

Now if you return to the main page, you can see the following suggestion in the input box: Enter a keyword, KB number or AOT object path ($\ObjectType\Object or $\ObjectType\Object#element, e.g. $\Classes\Tax#post). If you suspect that something is wrong in a specific object or method, you can easily find all related fixes:

preRunModifyContract

I also noticed that in AX 2012 R3 you can open Issue Search for a given object directly from AOT.

AOTContextMenu

Just note that this currently works only for root elements, not for individual methods.

I never enjoyed searching for hotfixes on PartnerSource; Issue Search is so much better. I don’t say it’s perfect, but it’s evolving, together with the rest of Dynamics Lifecycle Services. And you can help it to evolve by sending some feedback to the product team – just use the smiling icon in the top right corner. Smile

I can tell you from my experience that they don’t ignore feedback (which, of course, doesn’t necessarily mean that your favorite feature will get on the top of their backlog).

Operand types are not compatible

$
0
0

I managed to produce a really confusing error in AX 2012. Look at the following piece of code:

MyExtendedDataType a, b;
a = b;

Pretty boring, isn’t it? There is nothing to fail… except that I got a compilation error: Operand types are not compatible with the operator. What?

Actually it become quite clear as soon as I simplified my code to this trivial example. There is clearly nothing wrong with my code, it must have something to do with the data type.

To make a long story short, I created a new extended data type of type enum and forgot to fill in the EnumType property. Linking the EDT to an enum instantly solved the problem.

But beware – the compiler won’t detect many other uses of this incorrectly-defined type. For example, MyExtendedDataType a = NoYes::Yes; is compilable X++ code even if MyExtendedDataType has empty EnumType.

Printing reports from code in AX2012

$
0
0

How to print a report from X++ and change print settings in AX2012?

I have a concrete example for you – it should give a good idea how to do that even if your situation is a little bit different.

SrsReportRunController          controller = new SrsReportRunController();
SysUserLicenseCountRDPContract  rdpContract = new SysUserLicenseCountRDPContract();
SRSPrintDestinationSettings     settings;
 
// Define report and report design to use
controller.parmReportName(ssrsReportStr(SysUserLicenseCountReport, Report));
// Use execution mode appropriate to your situation
controller.parmExecutionMode(SysOperationExecutionMode::ScheduledBatch);
// Suppress report dialog
controller.parmShowDialog(false);
 
// Explicitly provide all required parameters
rdpContract.parmReportStateDate(systemDateGet());
controller.parmReportContract().parmRdpContract(rdpContract);
 
// Change print settings as needed
settings = controller.parmReportContract().parmPrintSettings();
settings.printMediumType(SRSPrintMediumType::File);
settings.fileFormat(SRSReportFileFormat::Excel);
settings.fileName(@'\\share\UserLicenseCount.xlsx');
 
// Execute the report
controller.startOperation();

Lines for multiple headers

$
0
0

The goal of today’s excercise is to create a form with two grids (containing headers and corresponding lines). The trick is that if you select multiple headers, the form will display lines for all of them.

The following form uses sales orders. Notice that selected orders are 1, 3 and 4 and displayed lines also belong to orders 1, 3 and 4:

Form

I’ve created two implementations: one for AX2012 and another for AX2009. They’re slightly different – I’m going to describe AX2012 first and then to mention changes done for AX2009. You can find a download link for both at the end.

The form has two data sources (SalesTable and SalesLine), which are not linked together. Filtering and query execution are not handled by AX; they’re explicitly triggered from selectionChanged() method on SalesTable data source.

For each selected header, a new range is added to SalesLine data source:

Ranges

You could use just a single range and create one long value by concatenating order IDs together, separated by space (e.g. “000001, 000002″), nevertheless it has a huge problem: the supported length of range value isn’t unlimited (I even managed to shoot down AX2012 client by using a sufficiently long value). Having several shorter values is also beneficial when displaying ranges in a grid (as in the previous picture).

The implementation itself is very straightforward; you shouldn’t have any problems to understand what’s going on:

public class FormRun extends ObjectRun
{
    MultiSelectionHelper headerSelection;
    QueryBuildDataSource lineQueryDs;
}
 
public void init()
{
    super();
 
    // Initialize MultiSelectionHelper for later use
    headerSelection = MultiSelectionHelper::construct();
    headerSelection.parmDatasource(salesTable_ds);
 
    // For convenient access to lines' QueryBuildDataSource
    // (in AX2012, you could use salesLine_ds.queryBuildDataSource())
    lineQueryDs = salesLine_ds.query().dataSourceNo(1);
}
 
public void selectionChanged() // SalesTable datasource
{
    super();
    SalesLine_ds.executeQuery();
}
 
public void executeQuery() // SalesLine datasource
{
    lineQueryDs.clearRanges();
    this.filterBySelectedHeaders();
 
    super();
}
 
void filterBySelectedHeaders() // SalesLine datasource
{
    SalesTable st = headerSelection.getFirst();
 
    while (st.RecId)
    {
        // Add one range for each selected SalesId
        lineQueryDs.addRange(fieldNum(SalesLine, SalesId)).value(queryValue(st.SalesId));
        st = headerSelection.getNext();
    }
}

In AX2009, we don’t have selectionChanged() method, therefore I combined markChanged() method and a delayed link between data sources, which calls lines’ executeQuery() on header selection. I just had to remove the dynamic link, otherwise AX would show lines for just a single header.

You can download both versions here: LinesForMultipleHeaders.zip.

Size matters

$
0
0

X++ normally doesn’t distinguish letter case – you can call warning(“x”) or WaRnInG(“x”) and both will do the same (although your colleagues may not be as forgiving as the X++ compiler). Nevertheless the same doesn’t apply to .NET Interop from X++ – although you can use System.Math::Cos(0), for example, you’ll get a compilation error if you try to call System.Math::cos(0).

There are even situations when a change in casing results in a call of a different method. Look at the following piece of code – it causes a .NET exception, catches it, converts it to a string and sends it to infolog:

try
{
    System.IO.File::Open('no such file', System.IO.FileMode::Open);
}
catch (Exception::CLRError)
{
    error(CLRInterop::getLastException().ToString());
}

The output should be something like: “System.Reflection.TargetInvocationException: Exception has been thrown by the target of invocation. —> System.IO.FileNotFoundException: Could not find file ‘C:\Windows\system32\no such file.’ (…)”

Now change ToString() to tostring(). The output will be “Class CLRObject”.

What happened? What we get from CLRInterop::getLastException() is an instance of CLRObject (X++) class, which represents a .NET object (FileNotFoundException in this case). When we call ToString() method, AX is smart enough to recognize that FileNotFoundException contains such a method and calls it. But if we call tostring(), AX can’t use the method on FileNotFoundException due to case sensitivity. But there is toString() method on the CLRObject class itself (inherited from Object) that can be called without issues. Therefore we’re not dealing with a single method returning different results, what’s called are methods on completely different objects.

It may be a bit confusing but it makes sense.

A required device isn’t connected

$
0
0

Microsoft recently released Dynamics AX 2012 R2 Solution Demo Package V4, i.e. a new version of the virtual machine with Dynamics AX and related components installed and configured. The version of AX there is 2012 R2 CU7.

I successfully downloaded, unpacked and imported VM A (30 GB), but when I tried to boot it, I got the following error:

0xc000000e A required device isn’t connected or can’t be accessed

I tried few changes in settings and even different versions of Hyper-V to no avail. Finally I tried to fix the master boot record and other boot data – and that helped!

Here are the instructions:

  1. Mount a Windows Server 2012 installation disc or a disc image (in VM settings). Ensure yourself that CD is the first boot device (section BIOS in Settings).
  2. Start the VM and confirm the boot from CD.
  3. Select/confirm a language, keyboard layout and so on.
  4. Don’t click Install now – choose Repair your computer instead.
  5. Click Troubleshoot and than Command Prompt
  6. Call these commands:
    bootrec /fixmbr
    bootrec /fixboot
    bootrec /rebuildbcd
  7. Confirm the Window installation to be added to boot.
  8. Reboot the VM and let it boot from disk.

A user session cannot be created

$
0
0

I installed AX2012 R2 CU7 with the demo data downloaded from Microsoft Dynamics AX 2012 Solution Demos on PartnerSource. When I attempted to start the AX client, it threw the following error:

A user session on the server could not be created. Try to start the client again. If the problem continues, contact the Microsoft Dynamics AX administrator.

I also found the following message in the Event Log:

Unable to connect to an Application Object Server. A company name or server name may not be specified correctly in the client configuration, or the server specified may not be available.

I checked the setup and everything was all right. I also looked on internet, just to find that few other people ran into the same error – but I didn’t find any solution.

I thought that “user session on the server could not be created” might have referred to missing permissions for execution of CREATEUSERSESSIONS stored procedure but it wasn’t the case either.

Then I used the SQL Server Profiler to obtain statements sent to database – as expected, CREATEUSERSESSIONS was called. I copied the command (with all parameters) and executed it by myself. And it failed! The error was:

Procedure or function CREATEUSERSESSIONS has too many arguments specified.

It was easy then to find out that the extra argument was a partition ID. Partitions were introduced in AX2012 R2 and the database obviously was from a pre-R2 environment.

There are two data set on Microsoft Dynamics AX 2012 Solution Demos:

  • Demo Data (used to restore VM-A v3 to original demo data state)
  • Demo Data (used to restore VM-A v4 to original demo data state)

Accidentally, I downloaded v3 without noticing there is another version available. When I downloaded and installed v4, everything started to work. I thought that v3 version was built upon AX2012 R2 too, however it doesn’t seem to be the case with the database.

The lesson learned isn’t only that I should be more careful about what I’m installing. The way how I managed to identify the problem is important as well and might help in other cases in future.

Creating sales orders via AIF in AX2012

$
0
0

I was asked for an example how to create a sales order through AIF document services (SalesSalesOrderService). Here is my minimalist variant in C# (for AX2012 and demo data):

var line = new AxdEntity_SalesLine()
{
    ItemId = "D0001",
    SalesQty = 42,
    SalesUnit = "ea"
};
 
var order = new AxdEntity_SalesTable()
{
    CustAccount = "US-003",
    PurchOrderFormNum = "xyz",
    ReceiptDateRequested = DateTime.Now.Date,
    SalesLine = new AxdEntity_SalesLine[] { line }
};
 
var orderList   = new AxdEntity_SalesTable[] { order };
var callContext = new CallContext() { Company = "USMF" };
var client      = new SalesOrderServiceClient();
 
try
{
    client.create(callContext, orderList);
    client.Close();
}
catch
{
    client.Abort();
    throw;
}

It doesn’t have to work for you out of the box (for example, you may have additional fields required by the contract), but it should give you an idea how it looks like, without adding unnecessary complexity.

If you’re not familiar with AIF document services, you may want to look at AX 2012 Documentation Resources for AIF and Services.

Data contract serialization from X++

$
0
0

You can decorate classes in AX2012 with DataContractAttribute in much the same way as in .NET with System.Runtime.Serialization.DataContractAttribute. The same is true also for DataMemberAttribute.

This is a simple data contract class in X++:

[DataContractAttribute]
class ItemContract
{
    str name;
 
    [DataMemberAttribute("Name")]
    public str parmName(str _name = name)
    {
        name = _name;
        return name;
    }
}

In .NET, you can serialize data contracts to XML (and deserialize from XML) with DataContractSerializer. What if you want to do the same with data contracts in X++?

I’m not aware of any such serializer in X++. You could write one, but it wouldn’t be a five-minute job. But there is a way how to actually use the .NET serializer.

As you surely know, AX2012 is able to compile X++ types to the Common Intermediate Language (CIL). Other “.NET” languages are compiled to CIL as well, which allows all of them to work together seamlessly. You can save a class written in a different language to a variable, call its methods, inherit from it and so on. (Don’t worry too much about differences between .NET, CLR, CIL, CLI etc. You just need to know that it’s all related to the same thing, usually called .NET).

When AX2012 generates CIL, .NET types for X++ classes and tables are created in Dynamics.Ax.Application assembly. For example, PriceDisc class is turned to Dynamics.Ax.Application.PriceDisc. If a piece of X++ code is executed in a CLR session, CIL types from Dynamics.Ax.Application are transparently used instead of original X++ types.

However, classes decorated by DataContractAttribute won’t be decorated by Dynamics.Ax.Application.DataContractAttribute in CIL. The X++ attribute is actually replaced by System.Runtime.Serialization.DataContractAttribute, therefore you’ll get a proper .NET data contract that can be serialized by DataContractSerializer. That’s likely what happens somewhere in AX kernel when you pass a data contract with parameters from AX to a report server, for instance.

By the way, I really like this approach. People from Microsoft managed to provide an easy way to define data contracts in X++ and still benefit from everything related to data contracts in .NET. Now it looks like an obvious solution, but who would have thought it about before?

Nevertheless what if we want to use the serializer from our own X++ code? First of all, let me introduce a C# method that will do all the serialization. All we have to do is to pass a data contract and we’ll get back the object serialized to XML.

using System;
using System.Runtime.Serialization;
using System.IO;
using System.Xml;
 
namespace Demo
{
    public class ContractSerializer
    {
        public static string Serialize(Object o)
        {
            Type type = o.GetType();
 
            if (!Attribute.IsDefined(type, typeof(DataContractAttribute)))
            {
                throw new ArgumentException(String.Format("{0} is not a data contract", type.FullName));
            }
 
            using (var stringWriter = new StringWriter())
            using (var xmlWriter = new XmlTextWriter(stringWriter) { Formatting = Formatting.Indented })
            {
                new DataContractSerializer(type).WriteObject(xmlWriter, o);
                return stringWriter.GetStringBuilder().ToString();
            }
        }
    }
}

You can put the class to a class library, add it to AOT and deploy it to both client and server.

Then we need to ensure ourselves that our X++ code will run in CIL. We’ll utilize SysOperation framework for that purpose:

class SerializationDemo extends SysOperationServiceController
{
    public static void main(Args args)
    {
        SerializationDemo demo = new SerializationDemo();
 
        demo.parmClassName(classStr(SerializationDemo));
        demo.parmMethodName(methodStr(SerializationDemo, serializeToInfolog));
        demo.parmExecutionMode(SysOperationExecutionMode::Synchronous);
 
        demo.run();
    }
 
    [SysEntryPointAttribute]
    public void serializeToInfolog()
    {
        if (!xSession::isCLRSession())
        {
            throw error("Must run CIL!");
        }
        // Here we'll do the job
    }
}

Do not omit the verification that we’re really in a .NET session. If the code ran in X++, it wouldn’t work, because we can’t simply pass an X++ object to a .NET method. If the code don’t execute in CIL, go to Tools > Options > Development and tick Execute business operations in CIL. Also, don’t forget to generate CIL.

If we’re in a .NET session, we can simply create an instance of a data contract class and send it to our method. Let’s create some data:

private DirPersonInfoData getPersonData()
{
    DirPersonInfoData person = new DirPersonInfoData();
 
    person.parmPersonName("John Doe");
    person.parmPersonPrimaryEmail("john(at)doe.com");
    person.parmPersonUserId("jdoe");
 
    return person;
}

Unfortunately, if you try to pass it directly to the serialization method:

DirPersonInfoData person = this.getPersonData();
Demo.ContractSerializer::Serialize(person);

it will fail with the error “Microsoft.Dynamics.AX.ManagedInterop.Object is not a data contract”.

The problem here is that the generated code uses a proxy class, which is not what we need, because proxy classes are not generated with attributes. Let’s tell AX that we’re working with a .NET object:

DirPersonInfoData person = this.getPersonData();
System.Object clrPerson = CLRInterop::getObjectForAnyType(person);
;
Demo.ContractSerializer::Serialize(clrPerson);

Now let’s put it together and finalize serializeToInfolog() method by adding exception handling and output to infolog:

[SysEntryPointAttribute]
public void serializeToInfolog()
{
    DirPersonInfoData person = this.getPersonData();
    System.Object clrPerson;
    System.Exception ex;
    str serialized;
 
    if (!xSession::isCLRSession())
    {
        throw error("Must run CIL!");
    }
 
    try
    {
        clrPerson = CLRInterop::getObjectForAnyType(person);
        serialized = Demo.ContractSerializer::Serialize(clrPerson);
    }
    catch (Exception::CLRError)
    {
        ex = CLRInterop::getLastException();
        throw error(ex.ToString());
    }
 
    info(serialized);
}

And this is how DirPersonInfoData will be serialized:

<DirPersonInfoData xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://schemas.datacontract.org/2004/07/Dynamics.Ax.Application">
    <parmPersonCommunicatorSignIn />
    <parmPersonName>John Doe</parmPersonName>
    <parmPersonPrimaryEmail>john(at)doe.com</parmPersonPrimaryEmail>
    <parmPersonUserId>jdoe</parmPersonUserId>
    <parmWorkerTitle />
</DirPersonInfoData>

The fact that the same X++ code can be executed once in a native AX session and once in a CLR session is very powerful and you usually don’t have to care about how it works. Of course, it may be sometimes confusing. For example, you need to know how your code is executed in a given moment to use the right debugger.

Here I showed how to leverage it even more. The solution works because we can define a type in X++ and use it later as a CLR type. But it also depend on the fact how the CIL generator deals with DataContractAttribute, which is something you can only find by looking into CIL. A CIL decompiler comes handy if you want to play with these things.

Replacement groups in AX 2012

$
0
0

AX 2012 introduced a new type of form control called “Replacement group”. It’s very handy, nevertheless quite a few developers still don’t know about it or are not sure how to use it effectively.

This post is not going to details; the intention is rather to show something simple, though still from end to end.

Let’s say that we want to create a form showing released products. We create a form with InventTable as the data source and with a grid.

We’re especially interested in the Product field, so we drag it from the data source and drop it to the grid.

Form-Product

Notice that the type of the control is ReferenceGroup – we’ll talk about it very soon.

Without changing anything, we can run the form; it successfully displays product numbers:

Let’s say that users insist on working with product names instead of product codes. Maybe they should have used a better convention for product codes, but that’s another topic. Now we have meet the request.

What should we do? Providing an edit method that would display names and find IDs from names specified by users? No, it’s much simpler with reference groups. Open properties of the reference group and change the value of ReplacementFieldGroup property from AutoIdentification to ProductInformation:

ReplacementFieldGroup

Save the form and open it again – it now shows product names. It’s that simple!

ProductNames

If the product was editable (which is not normally the case in InventTable), you would also get a lookup and would be able to select (or type) product names:

LookupItemName

You could also override the lookup, if you don’t like the default one.

Now we should look more closely at how it works.

First of all, look at the Product field in InventTable. Its base type is Int64 and extended data type is EcoResProductRecId. Properties of the EDT shows that ReferenceTable = EcoResProduct, therefore the Product field contains record IDs of EcoResProduct table.

If we used the Int64Edit control to display the Product field, we would see just numbers, which wouldn’t be very useful.

RecIds

That’s why we have Reference group controls. The reference group in our form is bound to the Product field (set in ReferenceField property), so that’s what gets saved to database. But we don’t display it to users – instead of that, we use a replacement field group. Field groups are defined on tables in AOT – here we can see the groups of InventTable that we used in our example:

InventTable-fieldGroups

AX looks at the field (or even fields) contained in the field group and displays them instead of the record ID. If you change a value of the replacement field, AX finds the corresponding RecId and put it to the underlying field.

As you can see, reference groups allow you to change fields displayed to users by a simple property change. Also notice that it has zero effect to data stored in database – it still refers to a record ID.

There is one last thing I would like to mention, because it often confuses people. You may have a reference group for a worker showing the worker name (e.g. the Sales responsible person in Sales order form). The control uses the AutoIdentification field group of the HcmWorker table. The group contains a single field, Person, which is a RecId of DirPerson table. AutoIdentification group on DirPerson is empty, so where the name comes from? The trick is that DirPerson table inherits from DirPartyTable, which contains the Name field in its AutoIdentification group. AX deals with all this complexity for you.


Printing dynamic parameters (AX 2012 / SSRS)

$
0
0

In addition to parameters specified in data contracts, reports can also use queries. It’s very handy, because users can specify filters and sorting in exactly the same way as they’re used to from other places in Dynamics AX, they can use query expressions and so on.

ReportDialog
RangeSelection

A relative common request is printing parameter values on the report itself. It’s easy with parameters defined in data contracts – an expression like =Parameters!UsersToInclude.Value will do the job. But the same approach doesn’t work with dynamic parameters (that’s how query parameters get represented in SSRS).

I recommend the following approach. Use a report data provider class – very often, it’s already in place. Add a new data table to hold information about query ranges and expose it to the report. I’m using an existing table, TmpSysQuery, so we don’t have to discuss the design of the table. In processReport(), we extract ranges from the current query and save them to our temporary table.

class LedgerJournalPostControlDP extends SRSReportDataProviderBase
{
    …
    TmpSysQuery tmpSysQuery;
}
 
[SRSReportDataSetAttribute(tablestr(TmpSysQuery))]
public TmpSysQuery getTmpSysQuery()
{
    select tmpSysQuery;
    return tmpSysQuery;
}
 
public void processReport()
{
    …
    tmpSysQuery = MyQueryUtils::rangesToTable(this.query());
}

Because the logic for extracting ranges from a Query object may be needed from other places as well, I’ve put it into a separate class. This is the content of the method:

public static TmpSysQuery rangesToTable(Query _query)
{
    QueryBuildDataSource    qbds;
    QueryBuildRange         queryBuildRange;
    TmpSysQuery             tmpSysQuery;
    LabelType               tableLabel;
    int                     occurrence;
    int                     dataSourceNo;
    int                     i;
 
    if (!_query)
    {
        return tmpSysQuery;
    }
 
    for (dataSourceNo = 1; dataSourceNo <= _query.dataSourceCount(); dataSourceNo++)
    {
        qbds = _query.dataSourceNo(dataSourceNo);
        if (qbds.enabled())
        {
            occurrence = SysQuery::tableOccurrence(_query, qbds.table(), dataSourceNo);
            tableLabel = tableId2pname(qbds.table()) + SysQuery::tableOccurrenceText(occurrence);
 
            for (i = 1; i <= qbds.rangeCount(); i++)
            {
                queryBuildRange = qbds.range(i);
 
                if (queryBuildRange.value() && queryBuildRange.status() != RangeStatus::Hidden)
                {
                    tmpSysQuery.clear();
                    tmpSysQuery.DataSourceNo = qbds.uniqueId();
                    tmpSysQuery.TableLabel   = tableLabel;
                    tmpSysQuery.FieldLabel   = fieldId2pname(qbds.table(), queryBuildRange.field());
                    tmpSysQuery.RangeValue   = queryBuildRange.value();
                    tmpSysQuery.insert();
                }
            }
        }
    }
 
    return tmpSysQuery;
}

Then we add a new data set in our report and show query ranges in a tablix in exactly the same way as other data:

PrintedRanges

Table inheritance roots

$
0
0

I’m preparing a talk about table inheritance in AX 2012 and I wanted to know how many hierarchies exist in standard AX. Therefore I looked for all tables with SupportsInheritance = Yes and Extends = “”. It’s more than I expected (collected in AX 2012 R3):

  • AgreementHeader
  • AgreementHeaderExt_RU
  • AgreementHeaderHistory
  • AgreementHeaderHistoryExt_RU
  • AgreementLine
  • AgreementLineHistory
  • AifDocumentFilter
  • AifEndpointActionValueMap
  • AifPort
  • BankLC
  • BankLCLine
  • CaseDetailBase
  • CatProductReference
  • CatUserReview
  • CatVendProdCandidateAttributeValue
  • CollabSiteLink
  • CustInterestTransLineIdRef
  • CustInvLineBillCodeCustomFieldBase
  • CustInvoiceLineTemplate
  • CustVendDirective_PSN
  • CustVendRoutingSlip_PSN
  • DIOTAdditionalInfoForNoVendor_MX
  • DirPartyTable
  • EcoResApplicationControl
  • EcoResCategory
  • EcoResInstanceValue
  • EcoResProduct
  • EcoResProductMasterDimensionValue
  • EcoResProductVariantDimensionValue
  • EcoResValue
  • FBGeneralAdjustmentCode_BR
  • HRPDefaultLimit
  • HRPLimitAgreementException
  • IntercompanyActionPolicy
  • KanbanQuantityPolicyDemandPeriod
  • MarkupMatchingTrans
  • MCRCustCategory
  • PaymCalendarRule
  • PayrollPayStatementLine
  • PayrollProviderTaxRegion
  • PayrollTaxEngineTaxCode
  • PayrollTaxEngineWorkerTaxRegion
  • PCConstraint
  • PCPriceElement
  • PCProductConfiguration
  • PCRuntimeCache
  • PCTableConstraintColumnDefinition
  • PCTableConstraintDefinition
  • PCTemplateAttributeBinding
  • RetailChannelTable
  • RetailPeriodicDiscount
  • RetailPeriodicDiscountLine
  • RetailProductAttributesLookup
  • RetailPubEcoResCategory
  • RetailPubRetailChannelTable
  • RetailReturnPolicyLine
  • RetailTillLayoutZoneReference
  • SysManagedCodeExpression
  • SysManagedCodeExpressionParameter
  • SysPolicyRule
  • SysPolicyRuleType
  • TAMFundCustCategory
  • TradeNonStockedConversionLog
  • TrvEnhancedData
  • UserRequest
  • VendRequest

Delete order line through AIF

$
0
0

The following code sample shows how to delete a line of an existing sales order through AIF from outside Dynamics AX 2012 . It uses a partial update, which means that we don’t have to send the whole document back to AX. As the documentation says, we have to include just the fields to change (none in my case) and “any fields required by the document” – that’s why I included fields such as PurchOrderFormNum. Note that you may have different fields set as mandatory.

Also notice how action properties are specified – we’re deleting the line, which means updating the order.

static void Main(string[] args)
{
    using (SalesOrderServiceClient client = new SalesOrderServiceClient())
    {
        EntityKey[] entityKeyList = EntityKeyForSalesId("SO00001");
 
        // Get the order to modify
        var order = client.read(new CallContext(), entityKeyList);
 
        // For demo, always the last order line is deleted
        var lastLine = order.SalesTable[0].SalesLine.Last();
 
        var salesLine = new AxdEntity_SalesLine()
        {
            RecId               = lastLine.RecId,
            RecIdSpecified      = true,
            SalesUnit           = lastLine.SalesUnit,
            action              = AxdEnum_AxdEntityAction.delete,
            actionSpecified     = true
        };
 
        var salesTable = new AxdEntity_SalesTable()
        {
            _DocumentHash           = order.SalesTable[0]._DocumentHash,
            PurchOrderFormNum       = order.SalesTable[0].PurchOrderFormNum,
            ReceiptDateRequested    = order.SalesTable[0].ReceiptDateRequested,
            action                  = AxdEnum_AxdEntityAction.update,
            actionSpecified         = true,
            SalesLine               = new[] { salesLine }
        };
 
        AxdSalesOrder newOrder = new AxdSalesOrder()
        {
            SalesTable = new[] { salesTable }
        };
 
        // Update the order
        client.update(new CallContext(), entityKeyList, newOrder);
    }            
}
 
// Helper method
private static EntityKey[] EntityKeyForSalesId(string salesId)
{
    KeyField field = new KeyField()
    {
        Field = "SalesId",
        Value = salesId
    };
 
    EntityKey key = new EntityKey()
    {
        KeyData = new[] { field }
    };
 
    return new[] { key };
}

Custom rules for Code Upgrade Tool

$
0
0

The Code Upgrade Tool allows you to detect patterns in X++ code and even to automatically change code. It’s especially useful for code upgrade, because you can easily upgrade legacy code. For example, CompanyInfo.CurrencyInfo field was replaced by CompanyInfo::standardCurrency() method in AX 2012. To upgrade your code, you would have to find and fix all occurrences, which would be tedious. Fortunately the Code Upgrade Tools knows how to detect this pattern and how to fix the code for you. And this is still an easy case – you would at least find the problem anyway, because your original code wouldn’t compile. In some other cases, code is still compilable in the new version, but it’s wrong for some reason and you need something more clever to identify it. Also note that the Code Upgrade Tool doesn’t try to fix all the issues – in some cases, it only reports them and it’s up to you to decide how the code should be upgraded.

The tool was first introduced as a separate download for AX 2012 RTM and integrated to the standard installation in version R2. The original name was Code Upgrade Tool, it’s called Code Upgrade Service on InformationSource and it doesn’t seem to have any separate name since integration to AX. I keep calling it Code Upgrade Tool and I recommend the same to you, because that’s how related assemblies and namespaces are named anyway.

I’m using AX 2012 R3 when writing this article, but it all works the same in AX 2012 R2, as far as I know.

To use the tool, first go to the developer workspace and open Tools > Code upgrade > Configure rules for code upgrade. If you don’t see any rules, click Manage code upgrade rules and then Browse. Choose Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Rules.dll file from Dynamics AX client bin folder (typically c:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin\) and press Import rules. This imports rules prepared by Microsoft for code upgrade to AX 2012. You can also configure which rules should be used.

RuleConfig

When running code conflict detection (Tools > Code upgrade > Detect code upgrade conflicts), you can detect issues by ticking Create code upgrade patterns project checkbox and run automatic fixes by Auto-resolve code patterns.

DetectConflicts

The cool thing is that you can define your own rules and nothing prevents you from using them for other things than just code upgrade. You can use them for additional static analysis of your code, such as looking for dangerous code patters, and I’m sure it could be used for many different things if enough creative people played with it.

I’m going to show you how to write a simple rule. We found that a developer of our current codebase sometimes updated fields in InventDim table (such as InventLocationId) directly, i.e. not by creating a new InventDimId (or finding existing one). That’s very wrong and we want to be 100%-sure that not a single update like this remains in the application. Calls to update() method can be easily found by cross-references and I hoped to find references to doUpdate() in cross-reference tables as well, but unfortunately they’re not there. Therefore I decided to write a simple rule for Code Upgrade Tool to detect all such calls.

You can find some documentation for writing custom rules (Code Upgrade Tool User Guide [AX 2012]) and even code for rules prepared by Microsoft (downloadable from InformationSource), which will give you plenty of useful examples. The problem is that all this is for the beta version released for AX 2012 RTM and it won’t work with later versions. But let’s take a look anyway, because it’s still a good starting point.

Log on to InformationSource, switch to Services and download the Code Upgrade Service. Extract the archive and ignore everything except CodeUpgradeTool.Rules.zip and CodeUpgradeTool.Rules.Partner.zip. The first file contains code for standard rules implemented by Microsoft (don’t forget – it’s how it looked in the beta version) and CodeUpgradeTool.Rules.Partner.zip contains a starting project for new rules. Unpack CodeUpgradeTool.Rules.Partner.zip, remove write protection from files and open the Visual Studio project inside. By the way, I used Visual Studio 2013 to build the following example.

The project contains two classes: TestRuleSweeper (for rules that only looks for patterns) and TestRuleMutator (for rules that can actually fix identified patterns). I doubt I could write a completely generic fix for wrong updates of InventDim, therefore I’ll create a sweeper (and delete the mutator). This is TestRuleSweeper.cs:

namespace Microsoft.Dynamics.AX.Tools.CodeUpgradeTool.Rules.Partner.PartnerRules
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using Microsoft.Dynamics.AX.Framework.Xlnt.XppParser;
    using Microsoft.Dynamics.AX.Framework.Xlnt.XppParser.Pass2;
 
    [Author("")] 
    [Description("PartnerRuleSweeperTest")]
    [AutomaticallyFixable("No")]
    [Category("Test")]
    [Version("2009 -> 2012")]
    public class TestRuleSweeper : PipelineableSweeper
    {
        public TestRuleSweeper(IDiagnosticSink diagnostics, IMetadataProvider metadataProvider, IDictionary<string, string> parameters)
          : base(diagnostics, metadataProvider, parameters)
        {         
        }
 
        ////TODO: override the method from ASTSweeper that fits the rule
    }
}

We can go and override an appropriate “visit” method, such as VisitQualifiedCall():

protected override object VisitQualifiedCall(object payload, QualifiedCall qualifiedCall)
{
    return base.VisitQualifiedCall(payload, qualifiedCall);
}

Let’s not continue with this implementation, because it wouldn’t work anyway. Instead, we’ll update the project to work with the new version.

First of all, remove these assemblies from project references:

  • Microsoft.Dynamics.AX.Framework.Xlnt.XppParser
  • Microsoft.Dynamics.AX.Framework.Xlnt.XppParser.Pass2

and add a few new ones:

  • Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser
  • Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser.Pass2
  • Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Rules

You’ll find them in AX client bin folder (such as c:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin\).

References

Now a few things stop working, because they’ve changed in newer versions. We have to update them.

Firstly, remove old using statements and use new namespaces instead:

using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser;
using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser.Pass2;
using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Rules.ObjectModel;

Then throw away old attributes, such as [Description], and replace them with new ones, such as [RuleDescription] (you’ll find an example below).

The next thing you’ll notice is a compilation error related to the base class, PipelineableSweeper. Ignore the message about generic arguments; the fact is that even sweepers now extends the PipelineableMutator class. Change the class accordingly.

This is how the TestRuleSweeper template should look like these days:

namespace Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Rules.Partner.PartnerRules
{
    using System.Collections.Generic;
    using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser;
    using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser.Pass2;
    using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Rules.ObjectModel;
 
    [RuleCategory("")]
    [RuleTargetVersion("")]
    [RuleAuthor("")]
    [RuleDescription("")]
    [RuleType(AutomaticallyFixable.No)]
    public class TestRuleSweeper : PipelineableMutator
    {
        public TestRuleSweeper(IDiagnosticSink diagnostics, IMetadataProvider metadataProvider, IDictionary<string, string> parameters)
          : base(diagnostics, metadataProvider, parameters)
        {       
        }
    }
}

If you copy VisitQualifiedCall() from our previous implementation and compile the solution, you’ll get another error saying that VisitQualifiedCall() must returns Evaluation. Simply change the return type from object to Evaluation.

The remaining thing is to actually implement VisitQualifiedCall(). It’s also time to rename the class and fill values to the attributes. You may also want to change the namespace and the assembly name.

This is my complete class:

namespace Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Rules.Goshoom
{
    using System.Collections.Generic;
    using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser;
    using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser.Pass2;
    using Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Rules.ObjectModel;
 
    [RuleCategory("Inventory")]
    [RuleTargetVersion("2012")]
    [RuleAuthor("Martin Dráb")]
    [RuleDescription("InventDim.update() / doUpdate() shoukd not be updated by directly. InventDim::findOrCreate() should be used instead.")]
    [RuleType(AutomaticallyFixable.No)]
    public class InventDimUpdateSweeper : PipelineableMutator
    {
        public InventDimUpdateSweeper(IDiagnosticSink diagnostics, IMetadataProvider metadataProvider, IDictionary<string, string> parameters)
          : base(diagnostics, metadataProvider, parameters)
        {       
        }
 
        protected override Evaluation VisitQualifiedCall(object payload, QualifiedCall qualifiedCall)
        {
            Guard.ValidateIsNotNull(qualifiedCall, @"qualifiedCall");
 
            base.VisitQualifiedCall(payload, qualifiedCall);
            var qualifierType = qualifiedCall.Qualifier.GetExpressionType();
 
            if ((qualifiedCall.MethodName.Compare("doUpdate") || qualifiedCall.MethodName.Compare("update"))
                && (qualifierType != null && qualifierType.ToString().Compare("InventDim")))
            {
                this.AddDiagnosticWarning(1, qualifiedCall.Position, "Do not update InventDim table directly. Use InventDim::findOrCreate() instead.");
            }
 
            return qualifiedCall;
        }
    }
}

I don’t want to spend much time exploring the implementation of VisitQualifiedCall(), just notice how easy is to check whether the type of variable is InventDim. It’s because the compiler understands the code and makes this information available to Code Upgrade Tool as well.

It’s time to deploy our new rule. Build the solution, right-click the project and choose Open Folder in File Explorer. Open Debug\bin or Release\bin (depending on which configuration you used), find the DLL file with your rule and copy it to AX client bin folder.

Assembly

Now you can load the rule in exactly the same way as how you loaded Microsoft rules (return to the top of this post if you’re not sure how). Metadata such as author and description are copied from attributes of the class:

And you’re done! Now simply run conflict detection with Create code upgrade patterns project ticked let AX to do the rest for you.

UpgradeProject

One disadvantage of my implementation is that you won’t see actual place in code where to find the pattern. Microsoft rules place TODO comments to code to make it obvious and they have a nice utility class for it, unfortunately it’s an internal class and therefore it would take more effort to achieve the same thing.

Although this post was mainly about infrastructure and not about all the fancy rules you could write, I hope you see the potential by yourself. It may be really useful, whether you want to automatically change code during upgrade, find some patterns in your application or maybe something completely different.

Unfortunately I also have one bad news. When testing my new rule, I found it’s ignored in quite a few cases. You can find my analysis and some suggestions in the follow-up blog post: Objects ignored by Code Upgrade Tool.

Objects ignored by Code Upgrade Tool

$
0
0

When testing my new rule for code upgrade tool, I found that certain objects are skipped and rules are not checked for them. That makes the tool significantly less useful, because you can’t be sure that it found everything.

This blog post explains why it happens, but unfortunately doesn’t provide any reasonable workaround, except making the problem more visible. You should be simply aware of it until Microsoft provides a fix. (Please let me know if there already is a fix that I missed.)

I created a rule to check for a certain code pattern and I added the pattern to a few objects for testing. Although the rule seemed to work, some objects were not included (SalesLine table, for instance). Soon I discovered that it happened due to an exception thrown when checking rules for these objects. You can see the exception in the debugger, but it’s not displayed anywhere in the UI, unless you make the following one-lin change in SysUpgradeRuleRunner.processUtilElement():

if(Microsoft.Dynamics.AX.Framework.Tools.CodeUpgradeTool.Parser.Severity::Fatal == xppParserSeverity)
{
    error = diagnosticItem.ToString();
    error(error); // new code to display the error
    continue;
}

If you use the Code Upgrade Tool, I would recommend to add this modification to your application. It will at least let you know that an object was skipped.

This is the error message I got:

An error happened while executing PipelineTypeResolverPipelineEntryCould not load file or assembly ‘Microsoft.Dynamics.Retail.StoreConnect.TransAutomClient, Version=0.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35′ or one of its dependencies. The given assembly name or codebase was invalid. (Exception from HRESULT: 0x80131047)

What’s going on? First let me explain what happens when you run the Code Upgrade Tool:

  1. It finds all code in the current layer
  2. Each application object (such as a class) is loaded for analysis. Note that the whole object is loaded, not just the code in the current layer.
  3. In some cases, especially when a .NET type is used in code, AX loads all assemblies listed under AOT > References (so it can later look for types in these assemblies). It’s normally done just once and cached for subsequent calls.

The problem is that one of these references can’t ever be loaded. It’s the reference called TransAutomClient_x64, which refers to assembly Microsoft.Dynamics.Retail.StoreConnect.TransAutomClient for processor architecture AMD64. Dynamics AX client is a 32-bit process and it can’t load this assembly, therefore such as attempt must fail. I believe that Code Upgrade Tool would work without any problem if this reference was removed from AOT (but I can’t prove it, because it requires deleting a SYS-layer object).

AX tries to load .NET assemblies in several cases. For instance, if a .NET type is used in variable declaration or as method return type. It also happens when a static method is called on a .NET type, or when AX suspects it might be such a call. I noticed that even calls to table map methods (such as inventItemPrice.InventPriceMap::pcsPrice()) triggers loading of assemblies, because the syntax is exactly the same as for .NET calls (compare with System.Environment::GetLogicalDrives(), for example).

What can be done about it? I don’t think we can do anything by ourselves. What Microsoft should do is simply skipping assemblies that can’t be loaded. The process might fail later, if some code actually uses types defined in that assembly, but that’s inevitable. What happens now is much worse – it fails even if the library isn’t used by any code at all. There is still a potential issue that the tool loads assemblies even if no .NET types are needed (because of table maps calls, as above), but it wouldn’t really cause any harm if assembly loading behaved reasonably.

I simultaneously logged this issue on Connect (link). I really hope that Microsoft will address it soon, because it significantly affects usefulness of this great tool.

Viewing all 117 articles
Browse latest View live