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
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.