Thursday, November 28, 2013

Microsoft Dynamics AX 2012 – WCF Custom Services

Microsoft Dynamics AX 2012 – WCF Custom Services
 
Purpose: The purpose of this document is to illustrate how to develop and use Microsoft Dynamics AX 2012 WCF Custom Web Service in integration scenarios for composite (header-lines) business entities.
 
Challenge: In case the schema for business entity can be written as a simple data contract class with relevant data member attributes set you may want to develop .NET-like WCF Web Service exposing info about Microsoft Dynamics AX 2012 business entities. The idea would be to have Data Contract(s) and Service Contract classes, and not to use AIF (Application Integration Framework) infrastructure – Document Services
 
Solution: In Microsoft Dynamics AX, you can create custom services to expose X++ functionality to external clients. Any existing X++ code can be exposed as a custom service simply by adding an attribute. Microsoft Dynamics AX provides standard attributes that can be set on the data contract class and its members to automatically serialize and de-serialize data that is sent and received across a network connection. Many predefined types, such as collections and tables, are also supported
 
Walkthrough
 
For the purposes of this walkthrough I'm going to create WCF Custom Service for Inventory Adjustment Journal. The Data model for Inventory Adjustment Journal is pretty simple: InventJournalTable (journal header), InventJournalTrans (journal lines), InventDim (inventory dimensions). In particular I'll review header-line pattern in details
 
First let's discuss existing implementation of Inventory Adjustment Journal Document Service using AIF (Application Integration Framework)
 
There's a Query (AxdProfitLossJournal) behind Inventory Adjustment Journal Document Service (InventProfitLossJournalService)
 
Query (Axd)
 
 
This Query defines data set structure which consists of InventJournalTable (journal header), InventJournalTrans (journal lines), InventDim, etc. tables. Please note that Query also sets up the hierarchy of data sources, for example, InventJournalTrans (journal lines) is linked to InventJournalTable (journal header) 
 
Then I've created a project and included related to Document Service objects in there
 
Project (Axd)
 
 
The following artifacts will be the building blocks of Document Service:
<![if !supportLists]>-          <![endif]>Service node
<![if !supportLists]>-          <![endif]>Service, document object and data object classes (extends AifDocumentService, extends AifDocument, extends AfStronglyTypedDataContainer)
<![if !supportLists]>-          <![endif]>Data object type macro (AxdDCT)
<![if !supportLists]>-          <![endif]>Axd document class (extends AxdBase)
<![if !supportLists]>-          <![endif]>AxBC classes (extends AxInternalBase)
<![if !supportLists]>-          <![endif]>Job (GenerateXSDSchema)
 
Initially I included just some of artifacts listed above to the project, but we can also run Update Document Service Wizard and let the system group (other) supporting artifacts into a project as well
 
Update Document Service
 
 
This is how I generated another project which includes related AxBC classes, macros, etc.
 
Project (Axd)
 
 
Inventory Adjustment Document Service in Microsoft Dynamics AX 2012 exposes 3 operations (create, getChangedKeys, getKeys)
 
Service
 
 
For deployment I included it into InventServices Services Group and ultimately deployed Inbound port
 
Service Group
 
 
After that on the client side we can add Service Reference
 
Add Service Reference
 
 
And review the list of proxy classes generated in the client application
 
Proxy classes
 
 
As you can see it is a pretty long list where we have a lot of AIF specific classes describing types (EDTs), EntityKeys, etc. as well as Service Client class and Service Operations Request/Response classes
 
You can review a related walkthrough on how to create AIF Document Service in Microsoft Dynamics AX 2012 here: http://ax2012aifintegration.blogspot.com/2012/05/microsoft-dynamics-ax-2012-application.html. In this walkthrough you can also find an example (Source code) of how to consume it in .NET client application   
 
So now after we reviewed how we work with AIF Document Services it is time to switch to WCF Custom Services and review how we can develop WCF Custom Service for Inventory Adjustment Journal
 
First off we will look at InventJournalTable (journal header) and InventJournalLine (journal lines), and determine the required minimum of data which is required to create respective Data Contracts. For this purpose I'll inspect the list of mandatory fields on these tables and take a look at AutoReport field group to understand what fields I want to expose through Data Contacts
 
InventJournalTable 
 
 
For InventJournalTable (journal header) table it would make sense at least to include JournalId, JournalNameId and Description into journal header Data Contract 
 
InventJournalLine
 
 
For InventJournalTrans (journal lines) table it would make sense at least to include JournalId, LineNum, TransDate, ItemId and Qty into journal line Data Contract 
 
Please see below the Source code for Data Contracts I created for Journal header and Journal line
 
Data Contract (Journal header)
 
[DataContractAttribute]
public class InventAdjustJournalContract
{
    InventJournalId     journalId;
    InventJournalNameId journalNameId;
    JournalDescription  description;
 
    List                journalLines;
}
[DataMemberAttribute]
public InventJournalId JournalId(InventJournalId _journalId = journalId)
{
    journalId = _journalId;
    return journalId;
}
[DataMemberAttribute]
public InventJournalNameId JournalNameId(InventJournalNameId _journalNameId = journalNameId)
{
    journalNameId = _journalNameId;
    return journalNameId;
}
[DataMemberAttribute]
public JournalDescription Description(JournalDescription _description = description)
{
    description = _description;
    return description;
}
[AifCollectionTypeAttribute('return', Types::Class, classStr(InventAdjustJournalLineContract)), DataMemberAttribute]
public List JournalLines(List _journalLines = journalLines)
{
    journalLines = _journalLines;
    return journalLines;
}
 
Data Contract (Journal line)
 
[DataContractAttribute]
public class InventAdjustJournalLineContract
{
    InventJournalId     journalId;
    LineNum             lineNum;
    JournalTransDate    transDate;
    ItemId              itemId;
    InventQtyJournal    qty;
}
[DataMemberAttribute]
public InventJournalId JournalId(InventJournalId _journalId = journalId)
{
    journalId = _journalId;
    return journalId;
}
[DataMemberAttribute]
public LineNum LineNum(LineNum _lineNum = lineNum)
{
    lineNum = _lineNum;
    return lineNum;
}
[DataMemberAttribute]
public JournalTransDate TransDate(JournalTransDate _transDate = transDate)
{
    transDate = _transDate;
    return transDate;
}
[DataMemberAttribute]
public ItemId ItemId(ItemId _itemId = itemId)
{
    itemId = _itemId;
    return itemId;
}
[DataMemberAttribute]
public InventQtyJournal Qty(InventQtyJournal _qty = qty)
{
    qty = _qty;
    return qty;
}
 
Please note that I used parm method pattern to create get/set methods for each field, but I didn't use "parm" prefix because in .NET client application these methods will represent properties on Data Contract class (and, for example, I want to have "ItemId" instead of "parmItemId" property name)
Also you probably noticed that as a part of Journal header Data Contract I introduced List variable called journalLines, this is because I want to encapsulate journal lines into journal header, so when I pass journal header its lines will be also passed along. This defines the hierarchy of data sources in data set (journal contains lines) and resembles Query we have behind AIF Document Service
 
Finally I'll mention that I use EDTs in Data Contact classes and these types will be automatically translated into appropriate .NET types
 
Now we can develop Service Contract class to expose necessary operations. For the sake of this walkthrough I decided to expose the following operations:
<![if !supportLists]>-          <![endif]>getJournal: retrieves a single journal by JournalId
<![if !supportLists]>-          <![endif]>getRecentJournal: retrieves a list of 10 most recent journals
<![if !supportLists]>-          <![endif]>createJournal: creates journal header and related journal lines
<![if !supportLists]>-          <![endif]>createJournalLines: creates a list of journal headers and/or a list of journal lines associated with already existing journal headers or journal headers being created at the time
 
Let's review Service Contract implementation below
 
Service Contract
 
class InventAdjustJournalService
{
}
[SysEntryPointAttribute(true)]
public InventAdjustJournalContract getJournal(InventJournalId _journalId)
{
    InventAdjustJournalContract     journal;
    InventAdjustJournalLineContract journalLine;
    InventJournalTable              inventJournalTable;
    InventJournalTrans              inventJournalTrans;
    List                            journalLines;
    ;
 
    inventJournalTable = InventJournalTable::find(_journalId);
 
    if (inventJournalTable)
    {
        journal = new InventAdjustJournalContract();
 
        journal.JournalId(inventJournalTable.JournalId);
        journal.JournalNameId(inventJournalTable.JournalNameId);
        journal.Description(inventJournalTable.Description);
 
        journalLines = new List(Types::Class);
 
        while select inventJournalTrans
            index hint LineIdx
                where inventJournalTrans.JournalId == inventJournalTable.JournalId
        {
            journalLine = new InventAdjustJournalLineContract();
 
            journalLine.JournalId(inventJournalTrans.JournalId);
            journalLine.TransDate(inventJournalTrans.TransDate);
            journalLine.ItemId(inventJournalTrans.ItemId);
            journalLine.LineNum(inventJournalTrans.LineNum);
            journalLine.Qty(inventJournalTrans.Qty);
 
            journalLines.addEnd(journalLine);
        }
 
        journal.JournalLines(journalLines);
    }
 
    return journal;
}
[AifCollectionTypeAttribute('return', Types::Class, classStr(InventAdjustJournalContract)), SysEntryPointAttribute(true)]
public List getRecentJournals()
{
    InventAdjustJournalContract     journal;
    InventAdjustJournalLineContract journalLine;
    InventJournalTable              inventJournalTable;
    InventJournalTrans              inventJournalTrans;
    List                            journals;
    List                            journalLines;
 
    journals = new List(Types::Class);
 
    //10 most recent journals
    while select firstOnly10 inventJournalTable
        order by CreatedDateTime desc
            where inventJournalTable.JournalType == InventJournalType::LossProfit
    {
       journal = new InventAdjustJournalContract();
 
        journal.JournalId(inventJournalTable.JournalId);
        journal.JournalNameId(inventJournalTable.JournalNameId);
        journal.Description(inventJournalTable.Description);
 
        journalLines = new List(Types::Class);
 
        while select inventJournalTrans
            index hint LineIdx
                where inventJournalTrans.JournalId == inventJournalTable.JournalId
        {
            journalLine = new InventAdjustJournalLineContract();
 
            journalLine.JournalId(inventJournalTrans.JournalId);
            journalLine.TransDate(inventJournalTrans.TransDate);
            journalLine.ItemId(inventJournalTrans.ItemId);
            journalLine.LineNum(inventJournalTrans.LineNum);
            journalLine.Qty(inventJournalTrans.Qty);
 
            journalLines.addEnd(journalLine);
        }
 
        journal.JournalLines(journalLines);
 
        journals.addEnd(journal);
    }
 
    return journals;
}
[SysEntryPointAttribute(true)]
public InventJournalId createJournal(InventAdjustJournalContract _journal)
{
    InventAdjustJournalContract     journal;
    InventAdjustJournalLineContract journalLine;
    InventJournalTable              inventJournalTable;
    JournalTableData                journalTableData;
    InventJournalTrans              inventJournalTrans;
    JournalTransData                journalTransData;
    ListIterator                    literator;
    LineNum                         lineNum;
 
    try
    {
        ttsbegin;
 
        journalTableData = JournalTableData::newTable(inventJournalTable);
 
        inventJournalTable.clear();
        inventJournalTable.initValue();
        inventJournalTable.JournalId     = _journal.JournalId() ? _journal.JournalId() : journalTableData.nextJournalId();
        inventJournalTable.JournalType   = InventJournalType::LossProfit;
        inventJournalTable.JournalNameId = _journal.JournalNameId() ? _journal.JournalNameId() :
        journalTableData.journalStatic().standardJournalNameId(inventJournalTable.JournalType);
        inventJournalTable.initFromInventJournalName(InventJournalName::find(inventJournalTable.JournalNameId));
 
        if (_journal.Description())
            inventJournalTable.Description = _journal.Description();
 
        if (inventJournalTable.validateWrite())
        {
            inventJournalTable.insert();
 
            literator = new ListIterator(_journal.JournalLines());
 
            while (literator.more())
            {
                journalLine = literator.value();
 
                lineNum++;
                journalTransData = journalTableData.journalStatic().newJournalTransData(inventJournalTrans, journalTableData);
 
                inventJournalTrans.clear();
                inventJournalTrans.initValue();
                inventJournalTrans.initFromInventJournalTable(inventJournalTable);
                inventJournalTrans.Qty = journalLine.Qty();
                inventJournalTrans.setInventDimId(InventDim::findOrCreateBlank().InventDimId);
                inventJournalTrans.initFromInventTable(InventTable::find(journalLine.ItemId()), false, false);
                inventJournalTrans.LineNum = lineNum;
 
                if (inventJournalTrans.validateWrite())
                {
                    journalTransData.insert();
                    journalTableData.journalTable().update();
                }
                else
                {
                    throw error("@SYS18447");
                }
 
                literator.next();
            }
        }
        else
        {
            throw error("@SYS18447");
        }
 
        ttscommit;
    }
    catch
    {
        return "";
    }
 
    return inventJournalTable.JournalId;
}
[AifCollectionTypeAttribute('_journals', Types::Class, classStr(InventAdjustJournalContract)),
 AifCollectionTypeAttribute('_journalLines', Types::Class, classStr(InventAdjustJournalLineContract)),
 SysEntryPointAttribute(true)]
public void createJournalLines(List _journals, List _journalLines)
{
    InventAdjustJournalContract     journal;
    InventAdjustJournalLineContract journalLine;
    InventJournalTable              inventJournalTable;
    JournalTableData                journalTableData;
    InventJournalTrans              inventJournalTrans;
    JournalTransData                journalTransData;
    ListIterator                    literatorJournal, literatorJournalLine;
 
    try
    {
        ttsbegin;
       
        literatorJournal = new ListIterator(_journals);
 
        while (literatorJournal.more())
        {
            journal = literatorJournal.value();
 
            journalTableData = JournalTableData::newTable(inventJournalTable);
 
            inventJournalTable.clear();
            inventJournalTable.initValue();
            inventJournalTable.JournalId     = journal.JournalId() ? journal.JournalId() : journalTableData.nextJournalId();
            inventJournalTable.JournalType   = InventJournalType::LossProfit;
            inventJournalTable.JournalNameId = journal.JournalNameId() ? journal.JournalNameId() :
            journalTableData.journalStatic().standardJournalNameId(inventJournalTable.JournalType);
            inventJournalTable.initFromInventJournalName(InventJournalName::find(inventJournalTable.JournalNameId));
 
            if (journal.Description())
                inventJournalTable.Description = journal.Description();
 
            if (inventJournalTable.validateWrite())
            {
                inventJournalTable.insert();               
            }    
            else
            {
                throw error("@SYS18447");
            }
           
            literatorJournal.next();
        }
       
        literatorJournalLine = new ListIterator(_journalLines);                         
 
        while (literatorJournalLine.more())
        {
            journalLine = literatorJournalLine.value();
 
            inventJournalTable = InventJournalTable::find(journalLine.JournalId());
           
            journalTableData = JournalTableData::newTable(inventJournalTable);
            journalTransData = journalTableData.journalStatic().newJournalTransData(inventJournalTrans, journalTableData);
 
            inventJournalTrans.clear();
            inventJournalTrans.initValue();
            inventJournalTrans.initFromInventJournalTable(inventJournalTable);
            inventJournalTrans.Qty = journalLine.Qty();
            inventJournalTrans.setInventDimId(InventDim::findOrCreateBlank().InventDimId);
            inventJournalTrans.initFromInventTable(InventTable::find(journalLine.ItemId()), false, false);
            inventJournalTrans.LineNum = inventJournalTrans::lastLineNum(inventJournalTable.JournalId) + 1;
 
            if (inventJournalTrans.validateWrite())
            {
                journalTransData.insert();
                journalTableData.journalTable().update();
            }
            else
            {
                throw error("@SYS18447");
            }
 
            literatorJournalLine.next();
        }
 
        ttscommit;
    }
    catch
    {
        return;
    }
}
 
Please note the following:
<![if !supportLists]>-          <![endif]>getJournal method accepts JournalId parameters and returns Data Contract object
<![if !supportLists]>-          <![endif]>getRecentJournals method doesn't have parameters and returns a List of Data Contract objects
<![if !supportLists]>-          <![endif]>createJournal method accepts Data Contract object and returns JournalId
<![if !supportLists]>-          <![endif]>createJournalLines method accepts 2 Lists of Data Contracts (Journal headers List and Journal lines List) and returns nothing
 
Please note that in order to work with collection types such as Lists, etc. you can leverage AifCollectionTypeAttribute attribute. Let's review couple of AifCollectionTypeAttribute attribute usage scenarios 
 
In case your method returns a List you will specify "return" as a first argument as shown below
 
AifCollectionTypeAttribute('return', Types::Class, classStr(InventAdjustJournalContract))
 
In case your method accepts a List as parameter you will specify "_parameter" as a first argument as shown below
 
AifCollectionTypeAttribute('_parameter', Types::Class, classStr(InventAdjustJournalContract))
 
Please note that "_parameter" in attribute definition will have to correspond to actual name of parameter for the method
 
[AifCollectionTypeAttribute('_journals', Types::Class, classStr(InventAdjustJournalContract)), SysEntryPointAttribute(true)]
public void createJournals(List _journals)
 
Also you can pass multiple Lists (parameters) to the method at once, to do this you will specify appropriate AifCollectionTypeAttribute attributes through comma (,) as shown below
 
[AifCollectionTypeAttribute('_journals', Types::Class, classStr(InventAdjustJournalContract)),
 AifCollectionTypeAttribute('_journalLines', Types::Class, classStr(InventAdjustJournalLineContract)),
 SysEntryPointAttribute(true)]
public void createJournalLines(List _journals, List _journalLines)
 
Please find more info about AifCollectionTypeAttribute here: http://technet.microsoft.com/en-us/library/aifcollectiontypeattribute.aspx
 
The interesting thing about createJournalLines method is that based on my design it allows to create journal headers AND/OR journal lines. This means that I can pass the list of journal headers OR list of journal lines OR 2 lists at once (AND). You have this flexibility with WCF Custom Services because typically you are not bound to predefined Query with hierarchical structure of data sources     
 
After I implemented Data Contracts and Service Contract
 
Project
 
 
I can now create Service
 
Service
 
 
And add necessary operations
 
Add Service operations
 
 
Then I'll deploy Service Group which includes Service
 
Service Group
 
 
As a next step in .NET client application I'll add Service reference
 
Add Service reference
 
 
And review the list of proxy classes generated in the client application
 
Proxy classes
 
 
As you can see the system generated less Proxy classes comparing to scenario with AIF Document Service (no extra classes for types (EDTs), etc.) and we still have Service client class, Service operations Request/Response classes, etc.
 
The very last step is to consume WCF Custom Service in .NET application
 
Let's review the Source code below
 
Source code: getJournal
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApplication2.ServiceReference1;
 
namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                InventAdjustJournalServiceClient client = new InventAdjustJournalServiceClient();
 
                CallContext context = new CallContext();
                context.Company = "USMF";
 
                InventAdjustJournalContract journal = client.getJournal(context, "X");
 
                Console.WriteLine("JournalId:" + journal.JournalId);
 
                foreach (InventAdjustJournalLineContract journalLine in journal.JournalLines)
                {
                    Console.WriteLine("ItemId:" + journalLine.ItemId);
                }
 
                Console.WriteLine("Success!");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.InnerException.Message);
            }
            
            Console.ReadLine();
        }
    }
}
 
As you can see you work with Data Contract classes in .NET similarly to X++. Please note that you can call method synchronously (getJournal) or asynchronously (getJournalAsync)
 
Source code: getRecentJournals
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApplication2.ServiceReference1;
 
namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                InventAdjustJournalServiceClient client = new InventAdjustJournalServiceClient();
 
                CallContext context = new CallContext();
                context.Company = "USMF";
 
                InventAdjustJournalContract[] journals = client.getRecentJournals(context);
 
                foreach (InventAdjustJournalContract journal in journals)
                {
                    Console.WriteLine("JournalId:" + journal.JournalId);
 
                    foreach (InventAdjustJournalLineContract journalLine in journal.JournalLines)
                    {
                        Console.WriteLine("ItemId:" + journalLine.ItemId);
                    }
                }
 
                Console.WriteLine("Success!");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.InnerException.Message);
            }
           
            Console.ReadLine();
        }
    }
}
 
Source code: createJournal
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApplication2.ServiceReference1;
 
namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                InventAdjustJournalServiceClient client = new InventAdjustJournalServiceClient();
 
                CallContext context = new CallContext();
                context.Company = "USMF";
 
                InventAdjustJournalContract journal = new InventAdjustJournalContract();
                journal.Description = "Alex's journal";
 
                InventAdjustJournalLineContract[] journalLines = new InventAdjustJournalLineContract[2];
               
                journalLines[0] = new InventAdjustJournalLineContract();
                journalLines[0].ItemId = "X1";
                journalLines[0].Qty = 1;
 
                journalLines[1] = new InventAdjustJournalLineContract();
                journalLines[0].ItemId = "X2";
                journalLines[0].Qty = 1;
 
                journal.JournalLines = journalLines;
 
                string journalId = client.createJournal(context, journal);
                
                Console.WriteLine("JournalId:" + journalId);
                 
                Console.WriteLine("Success!");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.InnerException.Message);
            }
           
            Console.ReadLine();
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApplication2.ServiceReference1;
 
namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                InventAdjustJournalServiceClient client = new InventAdjustJournalServiceClient();
 
                CallContext context = new CallContext();
                context.Company = "USMF";
 
                InventAdjustJournalContract journal = new InventAdjustJournalContract();
                journal.Description = "Alex's journal";               
 
                List<InventAdjustJournalLineContract> journalLinesList = new List<InventAdjustJournalLineContract>();
 
                InventAdjustJournalLineContract journalLine;
               
                journalLine = new InventAdjustJournalLineContract();
                journalLine.ItemId = "X1";
                journalLine.Qty = 1;
 
                journalLinesList.Add(journalLine);
 
                journalLine = new InventAdjustJournalLineContract();
               journalLine.ItemId = "X2";
                journalLine.Qty = 1;
 
                journalLinesList.Add(journalLine);
 
                journal.JournalLines = journalLinesList.ToArray();
 
                string journalId = client.createJournal(context, journal);
                
                Console.WriteLine("JournalId:" + journalId);
                 
                Console.WriteLine("Success!");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.InnerException.Message);
            }
           
            Console.ReadLine();
        }
    }
}
 
The interesting thing about createJournal method is that in first code sample I used static array of journal lines (2), but in a real world scenario you may be dealing with dynamic array and you may not know exact number of elements in it upfront, that's why I provided a second code sample where I use List to add as many elements as I need and then convert List into Array by using List.ToArray() method  
 
Remark: In createJournal method please make sure you assign appropriate InventDimId to avoid "Inventory dimension Site is mandatory and must consequently be specified.", etc.
 
Please find more info about using Custom Services in Microsoft Dynamics AX 2012 here: http://technet.microsoft.com/en-us/library/hh509052.aspx
 
Summary: This document described how to develop and use Microsoft Dynamics AX 2012 WCF Custom Web Service in integration scenarios for composite (header-lines) business entities. In particular we reviewed how to develop WCF Custom Service for Inventory Adjustment Journal
 
Author: Alex Anikiev, PhD, MCP
 
Tags: Microsoft Dynamics ERP, Microsoft Dynamics AX 2012, Integration, WCF, Custom Service, AIF, Application Integration Framework, Document Service.
 
Note: This document is intended for information purposes only, presented as it is with no warranties from the author. This document may be updated with more content to better outline the concepts and describe the examples.