To further highlight the benefits of writing coding decoupled from the user interface and the use of data contracts, we will create a service and use it in a C# project.
Writing services in AX is straightforward; for example, to create a service that returns "Hello world", we simply need to return the "Hello world" string. We do have to describe the service to the outside world, however, and this is done by a decoration at the method header.
Firstly, we need to tell the compiler that this is a web method, and the return type. In the case of the Hello world example, it is simply the following:
[AifCollectionTypeAttribute('return', Types::String), SysEntryPointAttribute(true)]
The AifCollectionAttribute
determines that the return is of type string, and SysEntryPointAttribute
states that this is a web method.
A complete method that returns Hello world is as follows:
[AifCollectionTypeAttribute('return', Types::String), SysEntryPointAttribute(true)] public Name helloWorld() { return "Hello world"; }
We can add parameters too, so to return "Hello world my name is <parameter>", we would write this code:
[AifCollectionTypeAttribute('return', Types::String), SysEntryPointAttribute(true)] public Name helloWorldMyName(Name _myName) { return strFmt("Hello world my name is %1", _myName); }
Apart from the decoration, this is just like any other X++ method.
Let's try this and test this by creating a service and calling it from Visual Studio.
ConFMSVehicleServices
and create the two methods in the same way as you just saw.ConFMSVehicleServices
. Enter this in the Class property as well.ContosoFMS
in Namespace. This acts as a scope where the service only has to be unique within this namespace.ConFMSServices
.Once this is complete, you should see an info log with various information messages, including this line:
The port 'ConFMSServices' was deployed successfully.
Should there be a compilation error anywhere in the X++ code, AX will not be able to complete the CIL generation from the p-code. Also, if you receive an error that doesn't relate to a specific X++ compilation error, you will need to generate a full CIL, which is done from the Build menu.
This also created an inbound port within the Application Integration Framework (AIF). To see this, navigate to System administration | Setup | Services and Application Integration Framework | Inbound ports.
You should see a record for your port, as shown in the following screenshot:
We will use the WSDL URI when including a reference in our Visual Studio project, which will differ from the preceding example, based on the AOS on which it was deployed.
To create the test project, please follow these steps:
ConFMSVehicleServicesTest
in Name and press OK.http://AX2012R2A:8101/DynamicsAx/Services/ConFMSServices
in our case.ConFMSServices
, and on expanding this node, you will see the Operations you added.ConFMSServicesReference
in Namespace and press OK.Program.cs
CS file from the Solution Explorer, which has one static method, named Main
.Main
method:ConFMSServicesReference.CallContext context = new ConFMSServicesReference.CallContext(); //context.LogonAsUser = "Contoso\administrator"; //context.Language = "en-gb"; //context.Company = "usmf"; ConFMSServicesReference.ConFMSVehicleServicesClient client = new ConFMSServicesReference.ConFMSVehicleServicesClient(); String responseMessage = client.helloWorld(context); Console.WriteLine(responseMessage); responseMessage = client.helloWorldMyName(context, "Tyler"); Console.WriteLine(responseMessage); responseMessage = client.helloWorldMyName(context, "Isabella"); Console.WriteLine(responseMessage); Console.ReadKey();
Since we have done all of the hard work, we just need to create a method that accepts the contract
and calls the update
class.
We will need to return the error should the validation fail, which means we have to interrogate the infoLog
object. Whenever we call info
, warning
, error
, checkFailed
, or throw exception
(plus others), AX will add this to the session global infoLog
object.
The data is stored in a container. A container is an unsorted, untyped set. There is no enumerator either, so we access its data through the conPeek
command. We can test whether it is null using the conNull
command.
We get the infoLog
data using infoLog.copy(from, to)
. This returns a container that will contain two or more elements from the range specified. The first is another container with one element of type integer. This describes the compatibility level of the data returned, and it should always be 1
. The subsequent elements are also a container each, containing the actual data from the requested infoLog
records.
Each infoLog
record container contains five elements:
1 |
An integer representing the exception level from the |
2 |
The message text. |
3 |
|
4 |
Class ID of the |
5 |
A container that holds the call stack. |
If the infoLog.copy(from, to)
command returns no data or the infoLog
object is empty, it returns a null container.
To get the current infoLog
line number (the last message), we use the infologLine()
command.
The following job highlights how we would use this to get the last message from the infoLog
object:
static void conGetInfoFromInfoLog(Args _args) { container infologData, infologInnerData; str message; info("Message 1"); info("Message 2"); infologData = infolog.copy(infologLine(), infologLine()); if(infologData != conNull()) { //get data (2nd element) from the outer container infologInnerData = conPeek(infologData, 2); //get message text (2nd element) from the inner container message = conPeek(infologInnerData, 2); info(message); } }
We just used a hardcoded number, where it would be ideal if there were a standard macro or function call that gets this information for us. In these cases, there isn't, so this will need to be documented for future upgrades. As it stands, we will need to verify that this code still works after an upgrade. We will use this technique to return validation errors, if any, to the caller.
To process the service request we need a ConFMSVehicleGroupChangeContract
object, and we will return a status.
The method is like what we did before, and is done by the following steps:
ConFMSVehicleServices
class, create a new method called updateVehicleGroup
.updateVehicle
method on the drop dialog form, and it should be as follows:[AifCollectionTypeAttribute('return', Types::String), SysEntryPointAttribute(true)] public str updateVehicleGroup (ConFMSVehicleGroupChangeContract _contract) { str returnMsg; container infologData, infologInnerData; ConFMSVehicleGroupChange change = ConFMSVehicleGroupChange::newFromContract(_contract); if(change.validate()) { change.run(); returnMsg = "OK"; } else { infologData = infolog.copy(infologLine(), infologLine()); if(infologData != conNull()) { //the data we need is in the 2nd element //of the outer container infologInnerData = conPeek(infologData, 2); //the message is in the 2nd element of the //inner container returnMsg = conPeek(infologInnerData, 2); } } return returnMsg; }
ConFMSVehicleServices
service and choose Add Operation.updateVehicleGroup
method and press OK.ConFMSServices
service group and choosing Deploy Service Group.We will simply update the project to run some tests on the service method and see yet another benefit of the pattern of decoupling the data contract from the object that processes it.
Before we write the test project, create a vehicle group, Fleet
, and ensure that we have a vehicle with V0001
as the vehicle ID, or change the following code so that it references a matching vehicle. We will test each combination to ensure that the validation works with the following steps:
using
command at the top:using ConFMSVehicleServicesTest.ConFMSServicesReference;
static
method as follows:public static void updateVehicleGroup() { CallContext context = new ConFMSServicesReference.CallContext(); ConFMSVehicleServicesClient client = new ConFMSVehicleServicesClient(); ConFMSVehicleGroupChangeContract contract = new ConFMSVehicleGroupChangeContract(); Console.WriteLine("Should work"); contract.vehicleGroupId = "Fleet"; contract.vehicleId = "V0001"; string msg = client.updateVehicleGroup(context, contract); Console.WriteLine(msg); Console.WriteLine("Vehicle Id should be invalid"); contract.vehicleGroupId = "Fleet"; contract.vehicleId = "ILB001"; msg = client.updateVehicleGroup(context, contract); Console.WriteLine(msg); Console.WriteLine("Vehicle group should be invalid"); contract.vehicleGroupId = "TLB001"; contract.vehicleId = "V0001"; msg = client.updateVehicleGroup(context, contract); Console.WriteLine(msg); Console.ReadKey(); }
Main
method to read like this:static void Main(string[] args) { Program.updateVehicleGroup(); }
In this example, we will create a service method that returns a list of vehicle group records as data contracts. Rather than returning a full list, we will use a technique so that the caller can specify a query that we will process within the service method.
The way in which we can pass a generic query definition is by using the class AifQueryCriteria
. This allows us to create a generic query definition, but in order to use it, we need to convert it to a Query
object.
The fact that AifQueryCriteria
is generic also means that there is no type-based validation when we come to use it; we could add elements for tables and fields that could cause our service method to fail. Although we can validate the object inside the service method, it doesn't help a developer who doesn't understand the data structures within AX.
Writing a data contract class to define the criteria can be complicated, especially if we need it to be flexible. So, a suitable solution to this would be to write a service method that provides a list of field names with their types.
The utility method we will create ranges on a Query
object from the criteria elements within an AifQueryCriteria
object. It does this by looping through each criteria element, finding or creating a data source on the Query
object, and adding the criteria element as a range to the data source. The queryValue
method ensures that the value added to the range is safe, and prevents code injection.
On the class ConFMSUtil
, create the following method:
public static Query buildQueryFromAifQueryCriteria( AifQueryCriteria _criteria) { counter idx; AifCriteriaElement element; Query query = new Query(); QueryBuildDataSource dataSource; QueryBuildRange range; for(idx = 1; idx <= _criteria.getCriteriaElementCount(); idx++) { element = _criteria.getCriteriaElement(idx); dataSource = SysQuery::findOrCreateDataSource(query, tableName2id(element.getDataSourceName())); switch(element.getOperator()) { case AifCriteriaOperator::Equal: range = SysQuery::findOrCreateRange(dataSource, fieldName2id(dataSource.table(), element.getFieldName())); range.value(queryValue(element.getValue1())); break; case AifCriteriaOperator::Greater: range = SysQuery::findOrCreateRange(dataSource, fieldName2id(dataSource.table(), element.getFieldName())); range.value(">" + queryValue(element.getValue1())); break; case AifCriteriaOperator::GreaterOrEqual: range = SysQuery::findOrCreateRange(dataSource, fieldName2id(dataSource.table(), element.getFieldName())); range.value(queryValue(element.getValue1())+".."); break; case AifCriteriaOperator::Less: range = SysQuery::findOrCreateRange(dataSource, fieldName2id(dataSource.table(), element.getFieldName())); range.value("<" + queryValue(element.getValue1())); break; case AifCriteriaOperator::LessOrEqual: range = SysQuery::findOrCreateRange(dataSource, fieldName2id(dataSource.table(), element.getFieldName())); range.value(".." + queryValue(element.getValue1())); break; case AifCriteriaOperator::NotEqual: range = SysQuery::findOrCreateRange(dataSource, fieldName2id(dataSource.table(), element.getFieldName())); range.value("!" + queryValue(element.getValue1())); break; case AifCriteriaOperator::Range: range = SysQuery::findOrCreateRange(dataSource, fieldName2id(dataSource.table(), element.getFieldName())); range.value(queryValue(element.getValue1())+".."+ queryValue(element.getValue2())); break; } } return query; }
Next, we need to create a data contract based on the table ConFMSVehicleGroup
. This is no different from the contract created earlier, and should be written as follows:
[DataContractAttribute] class ConFMSVehicleGroupContract { ConFMSVehicleGroupId vehicleGroupId; Description description; } [DataMemberAttribute] public ConFMSVehicleGroupId vehicleGroupId(ConFMSVehicleGroupId _vehicleGroupId = vehicleGroupId) { vehicleGroupId = _vehicleGroupId; return vehicleGroupId; } [DataMemberAttribute] public Description description(Description _description = description) { description = _description; return description; } public static ConFMSVehicleGroupContract construct() { return new ConFMSVehicleGroupContract(); }
To simplify its usage, we will add a method to the ConFMSVehicleGroup
table to construct the contract. Create the following method:
public ConFMSVehicleGroupContract contract() { ConFMSVehicleGroupContract contract; contract = ConFMSVehicleGroupContract::construct(); contract.vehicleGroupId(this.VehicleGroupId); contract.description(this.Description); return contract; }
The interesting part of this method is the decoration. We need to return a typed list of objects to the caller, and in AX, we do this using the following decoration:
[AifCollectionTypeAttribute('return', Types::Class, classStr(ConFMSVehicleGroupContract)), SysEntryPointAttribute(true)]
Even though we will return a List
object, the preceding lines of code will tell the caller what type of the list is ConFMSVehicleGroupContract[]
.
The equivalent code in C# is:
public ConFMSVehicleGroupContract[] vehicleGroups(...)
The method we will write next will perform the following:
AifQueryCriteria
object as its parameterAifQueryCriteria
object to a Query
objectList
objectList
objectThis is accomplished by the following code:
[AifCollectionTypeAttribute('return', Types::Class, classStr(ConFMSVehicleGroupContract)), SysEntryPointAttribute(true)] public List retrieveVehicleGroupQuery(AifQueryCriteria _criteria) { List groupList = new List(Types::Class); Query q; QueryRun qr; ConFMSVehicleGroup vehicleGroup; q = ConFMSUtil:: buildQueryFromAifQueryCriteria(_criteria)); qr = new QueryRun(q); while (qr.next()) { vehicleGroup = qr.get(tableNum(ConFMSVehicleGroup); groupList.addEnd(vehicleGroup.contract()); } return groupList; }
Finally, we need to add the method as an operation to the ConFMSVehicleServices
service and deploy the ConFMSServices
service group.
In this example, we will return a list of all vehicle groups that matches a specified criterion. To test this, we will need the following setup data in the ConFMSVehicleGroup
table:
Vehicle group |
Description |
---|---|
Fleet |
Fleet |
Group |
Description |
I-Artic |
Own fleet-Articulated truck |
I-Car |
Own fleet-Car |
I-Van |
Own fleet-Van |
J-Car |
J-Car |
We have updated the service, so the first step is to refresh the ConFMSServiceReference
service reference within the Visual Studio project we created earlier. To do this, right-click on it and select Update Service Reference.
Create a new method in Program.cs
as follows:
public static void retrieveVehicleGroups() { CallContext context = new ConFMSServicesReference.CallContext(); ConFMSVehicleServicesClient client = new ConFMSVehicleServicesClient(); ConFMSVehicleGroupContract[] vehicleGroups; QueryCriteria criteria = new QueryCriteria(); CriteriaElement criteriaElement = new CriteriaElement(); criteriaElement.DataSourceName = "ConFMSVehicleGroup"; criteriaElement.FieldName = "VehicleGroupId"; criteriaElement.Operator = Operator.Range; criteriaElement.Value1 = "I"; criteriaElement.Value2 = "J"; criteria.CriteriaElement = new CriteriaElement[1]; criteria.CriteriaElement[0] = criteriaElement; vehicleGroups = client.retrieveVehicleGroupQuery(context, criteria); foreach(ConFMSVehicleGroupContract contract in vehicleGroups) { Console.WriteLine("Vehicle Group: " + contract.vehicleGroupId + "(" + contract.description + ")"); } Console.ReadKey(); }
We used Operator.Range
as the operator because we couldn't use wildcards. To make the calls safe, we used the function queryValue
in the code that converts the AifQueryCriteria
object to a Query
object. This has the effect of replacing the *
with a literal, that is, I*
would return vehicle groups that have the ID I*
.
In the Main
method, replace the call to updateVehicleGroup
to call retrieveVehicleGroups
, instead. When it is run this time, you should get the following output:
You will see, from this demonstration, that it is quite simple to expose the Dynamics AX functionality as a service that can be consumed by other external applications throughout the organization. This could be on production control systems, door entry systems, tablets, phones, or any other type of device.
The work required to extend the solution outside of AX was minimal. We could very easily write a service method that allows us to change the vehicle status via a mobile device; we would just reuse the code that we wrote in the Using menu items to change the vehicle status section.
This is not a way to get free user CALs. When using code in this way, they are licensed as a device CAL. You can download the Dynamics AX 2012 licensing and pricing guide from http://www.microsoft.com/en-us/dynamics/resource-library.aspx?SortField1=Microsoft%20Dynamics%20AX&SortField2=Licensing.
3.142.40.32