Exposing the vehicle group logic as a custom web service

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.

Creating the service class and service

Let's try this and test this by creating a service and calling it from Visual Studio.

  1. Create a class called ConFMSVehicleServices and create the two methods in the same way as you just saw.
  2. Right-click on the Services node in your project and go to New | Service.
  3. Rename the service to ConFMSVehicleServices. Enter this in the Class property as well.
  4. Enter ContosoFMS in Namespace. This acts as a scope where the service only has to be unique within this namespace.
  5. Right-click on the service's Operations node and select Add Operation.

    Note

    AX will display a dialog with the two methods in the class. If they don't appear, check whether SysEntryPointAttribute was specified in the method and that it was public.

  6. Check both the method names and press OK.

    Note

    Next, we need a Service group. It is a group that is deployed through the AIF and is the lowest level by, which we can control its availability. It is appropriate to have one service group for each area of functionality.

  7. Right-click on the Service groups node and go to New | Service Group.
  8. Rename this group to ConFMSServices.
  9. We drag your service onto the new service group. We should now have the structure similar to what is shown in the following screenshot:
    Creating the service class and service
  10. We right-click on our service group and choose Deploy Service Group.

    Note

    This will cause AX to generate an incremental CIL and deploy our service as a WCF service. This may take up to 15 minutes, but if we have been building CILs frequently, it should happen within 2 minutes.

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:

Creating the service class and service

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.

Creating the Visual Studio test project

To create the test project, please follow these steps:

  1. Start Visual Studio (2010 or later) and go to File | New | Project....
  2. Select Console Application from the Visual C# node, which can be found under Installed Templates.
  3. Enter ConFMSVehicleServicesTest in Name and press OK.
  4. You should now have the Solution Explorer window open, which is normally docked on the right.
  5. On the References node, right-click and select Add Service Reference.
  6. Paste in the WSDI URI from the AIF port configuration, http://AX2012R2A:8101/DynamicsAx/Services/ConFMSServices in our case.
  7. Press Go.
  8. You should see our service group name, ConFMSServices, and on expanding this node, you will see the Operations you added.
  9. Enter ConFMSServicesReference in Namespace and press OK.
  10. Open the Program.cs CS file from the Solution Explorer, which has one static method, named Main.

    Note

    In order to call a web service in AX, we have to define a call context. This would normally define the security context and various login properties. We don't have to provide these details in this case, as we are logged on as a valid AX user, and will run this interactively.

  11. Write the following lines of code in the 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();
  12. Press F5 to run the project. You should see the following response in the console window:
    Creating the Visual Studio test project

Creating a service method to change the vehicle group

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.

Getting information from the infoLog object

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

2

The message text.

3

HelpUrl specified when the infolog was added. Usually, this is empty.

4

Class ID of the SysInfoAction class. 2071 is the default; it is the SysInfoAction_Editor class, which adds the Edit button to the infoLog.

5

A container that holds the call stack. element 2 of this is the string AOT reference to the class that triggered the infoLog message.

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);
    }
}

Tip

When we need to write snippets of code similar to what was just covered, it is often easier to write it first as a job to test that it works, before we use it in its correct place.

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.

Creating the updateVehicleGroup service method

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:

  1. On the ConFMSVehicleServices class, create a new method called updateVehicleGroup.
  2. The code is very similar to the method we wrote in the 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;
    }
  3. We now need to add this new service method as an operation to our service. Right-click on the ConFMSVehicleServices service and choose Add Operation.
  4. Check the updateVehicleGroup method and press OK.
  5. We now need to redeploy the service group, which is done as before—by right-clicking on the ConFMSServices service group and choosing Deploy Service Group.

Updating the Visual Studio service test project

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:

  1. In Visual Studio, using the same project you used earlier, right-click on ConFMSServicesReference, which is under Service References, and select Update Service Reference.

    Note

    We now have a new class, ConFMSVehicleGroupChangeContract, which we can use. We will use this to test the service.

  2. To reduce the typing, add a using command at the top:
    using ConFMSVehicleServicesTest.ConFMSServicesReference;
  3. Let's move the code around a little; create a new 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();
    }
  4. Update the code in the call to this in the Main method to read like this:
    static void Main(string[] args)
    {
        Program.updateVehicleGroup();
    }
  5. Press F5 to run the code. The result should be similar to the following screenshot:
    Updating the Visual Studio service test project

Creating a service method to return a list of vehicle groups

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.

Creating a utility method to convert an AifQueryCriteria object to a Query object

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;
}

Creating a vehicle group data contract

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;
}

Creating the service method

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:

  • Have an AifQueryCriteria object as its parameter
  • Convert the AifQueryCriteria object to a Query object
  • Process the query and add matching records as data contracts to a List object
  • Return the resulting List object

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

Note

If you have renamed or deleted any types, you must generate a full CIL, otherwise, the deploy action will generate an incremental CIL for us.

Using the new service within the Visual Studio test project

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();
}

Note

If the ConFMSVehicleGroupContract type does not exist, ensure that CIL is generated and the service group is redeployed.

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:

Using the new service within the Visual Studio test project

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.

Note

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.121.8