Creating Custom Dynamics AX Services

 

Version: Dynamics AX 2009.

 

Disclaimer

All code used below is meant for illustration purposes only and is not intended for use in production. The following disclaimer applies to all code used in this blog:

 

Copyright (c) Microsoft Corporation. All rights reserved. THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER. USE AND REDISTRIBUTION OF THIS CODE, WITH OR WITHOUT MODIFICATION, IS HEREBY PERMITTED.

 

Overview

In certain scenarios, X++ classes need to be exposed for consumption by external applications, for example, through WCF web services or through MSMQ. Custom services, which have been introduced with Microsoft Dynamics AX 2009, are intended exactly for that purpose.

However, since hardly any assumptions can be made about the purpose or semantics of these custom services or about the structure of the parameter types used by the published service operations, creating a custom service involves a little more work that creating a document-centric service with AIF’s Create document service wizard from a Dynamics AX query. In order to implement a custom service, you need to:

o Write a service implementation class

o Create a service interface

o Implement data objects – if necessary

The following sections walk you through a simple example of how you can build a Dynamics AX service from an X++ class named MyDataObjectService. Once you have encapsulated the business logic you would like to expose, you can use AIF’s standard tools and infrastructure to publish the service through the (supported) transport of your choice.

Writing a Service Implementation Class

A service implementation class is an X++ class; service implementation classes need not implement any X++ interfaces or extend any X++ super classes. The class definition of a sample service implementation class MyDataObjectService may look like this:

public class MyDataObjectService

{

}

 

One of MyDataObjectService’s service operations may look as shown in the following code snippet:

 

public MyDataObject CreateMyDataObject(str _s)

{

MyDataObject mdo; // see below for a definition of MyDataObject

;

 

mdo = new MyDataObject();

mdo.parmMyString(_s);

 

// do something with ‘mdo’, for instance persist it...

 

return mdo;

}

 

The input and return parameters of service operations must be either of primitive X++ types, or they must be instances of X++ classes that implement the X++ interface AifXmlSerializable.

Creating a Service Contract

In order to create a new service contract, we need to create a new AOT node under the AOT Services node. Let’s name the service contract MyDataObjectService, just like the service implementation class.

The newly created service contract has a few properties that need to be initialized before the service can be consumed by external clients:

o Service implementation class: MyDataObjectService

o Security key: Each service should have its own security key with the same name as the service, for example, MyDataObjectService; should have parent key Services in a functional area (for example, Accounts Receivable)

o Namespace (optional): XML namespace that should be used in the WSDL can be specified

o External name (optional): External name of the service

Finally, the service operations need to be added to the service contract; see for instance product documentation for additional details.

 

Implementing Data Objects

Service operations can automatically use primitive X++ types (such as int, str, etc.) as types for input and return parameters. X++ classes that are intended to be used as data objects – as input or return parameters for service operations – must implement the interface AifXmlSerializable. See the following snippet for an example:

 

public class MyDataObject implements AifXmlSerializable

{

str myString;

// more fields...

 

#define.MyDataObjectNS ('https://schemas.contoso.com/samples/MyDataObject')

#define.MyDataObjectRoot (‘MyDataObject’)

}

 

Note that arrays or any other X++ data structures are not primitive types, even if they only contain data of primitive types; thus, data objects must be defined for such constructs as well.

 

This is necessary to define the custom serialization to and the deserialization from XML for that class. Note that the methods serialize() and deserialize() must be inverse functions, since they use the same XML schema definition. In other words, it must be possible to deserialize an XML document that was created by serializing a data object and vice versa.

The code snippets below are examples for the implementation of these methods. For additional information about any of the implemented methods refer to the product documentation.

 

The method serialize() defines the serialization of the data object to XML. The code for serializing the class MyDataObject (as defined above) to XML may look similar to this:

 

AifXml serialize()

{

str xml;

XmlTextWriter xmlTextWriter;

;

 

#Aif

 

xmlTextWriter = XmlTextWriter::newXml();

 

// turn off indentation to reduce file size

xmlTextWriter.formatting(XmlFormatting::None);

 

// initialize XML document

xmlTextWriter.writeStartDocument();

 

// write root element

xmlTextWriter.writeStartElement2(#MyDataObjectRoot, #MyDataObjectNS);

 

// write custom data

xmlTextWriter.writeElementString('MyString', myString);

// more fields...

 

// serialize XML document into XML string

xmlTextWriter.writeEndDocument();

xml = xmlTextWriter.writeToString();

xmlTextWriter.close();

 

return xml;

}

 

The method deserialize() defines the deserialization of a data object from XML. The code for deserializing an instance of the class MyDataObject (as defined above) from XML may look like this:

 

void deserialize(AifXml xml)

{

XmlTextReader xmlReader;

;

 

xmlReader = XmlTextReader::newXml(xml);

 

// turn off Whitespace handling to avoid extra reads

xmlReader.whitespaceHandling(XmlWhitespaceHandling::None);

 

xmlReader.moveToContent();

while ((xmlReader.nodeType() != XmlNodeType::Element) && !xmlReader.eof())

xmlReader.read();

 

xmlReader.readStartElement3(#MyDataObjectRoot, #MyDataObjectNS);

if (!xmlReader.eof() && xmlReader.isStartElement())

{

myString = xmlReader.readElementString3('MyString', #MyDataObjectNS);

// more fields...

}

 

xmlReader.readEndElement();

xmlReader.close();

}

 

In X++, parm methods are used to define properties on a class. In data objects, all fields that are used for serialization or deserialization must be accessible through parm methods. Moreover, they must be optional and thus have a default value. Example:

 

public str parmMyString(str _myString = ‘’)

{

if (!prmisdefault(_myString))

{

myString = _myString;

}

return myString;

}

 

The method getRootName() returns the root name used for deriving names for service artifacts. An example for the implementation:

 

public AifDocumentName getRootName()

{

return #MyDataObjectRoot;

}

 

The method getSchema() returns the XML schema definition (XSD) that is used for serializing and deserializing the data object.

 

public AifXml getSchema()

{

str schema =

@'<?xml version="1.0"?>

<xsd:schema xmlns="https://schemas.contoso.com/samples/MyDataObject"

targetNamespace="https://schemas.contoso.com/samples/MyDataObject"

xmlns:xsd="https://www.w3.org/2001/XMLSchema"

elementFormDefault="qualified">

<xsd:complexType name="MyDataObjectType">

<xsd:sequence>

<xsd:element name="MyString" type="xsd:string" />

<!-- more fields... -->

</xsd:sequence>

</xsd:complexType>

<xsd:element name="MyDataObject" type="MyDataObjectType"/>

</xsd:schema>'

;

 

return schema;

}

 

XML schemas are used for validation, for example, to avoid the processing of invalid request messages. As a best practice, you should always use a modeling tool or an XML/XSD editor for generating the XML schema definitions rather than hand-crafting them.

 

Discovering Custom Services

Open the form Services (by navigating to Basic > Setup > Application Integration Framework > Services) and click Refresh. Once the form has refreshed its contents, the MyDataObjectService service displays along with the other services. It’s ready for use and can now be published for consumption by external service clients (see product documentation for details).