The Visual Studio project and its output have been added to the AOT, so the first stage of development is now complete. You can leave Visual Studio and switch to Microsoft Dynamics AX 2012. The project has been added to the Visual Studio Projects node in the AOT. As we have used C#, the project will be in the C Sharp Projects node.
Look for the DynamicsAxServices.WebServices.ZipCode project and expand some of the nodes to inspect it. It should appear as shown in the following screenshot:
As you can see in the preceding screenshot, this is divided into the following two main components:
In order to use the assemblies that have been created and stored in the AOT, we'll have to deploy them. Let's look at the options that are available.
When we create a project in Visual Studio and add it to the AOT, the following deployment options are available:
In our earlier example, we enabled deployment on the client and to the server because these are important in the context of services.
When you have enabled deployment to the server, the output of the Visual Studio project will be copied to the VSAssemblies
subfolder in the bin folder of the AOS directory. The default path is C:Program FilesMicrosoft Dynamics AX60Server<AOSServer>BinVSAssemblies
. After you have deployed assemblies to the server, you should restart the AOS so that they are loaded.
Hot swapping
When hot swapping is enabled on the AOS, restart is not needed after deployment. This feature is added for the convenience of developers but is not recommended for a production environment. For more info, check out the following article on MSDN: How to: Enable Hot Swapping of Assemblies (http://msdn.microsoft.com/en-us/library/gg889279.aspx).
When you have enabled deployment to the client, the output of the Visual Studio project will be copied to the following folder on the client: %localappdata%MicrosoftDynamics AxVSAssemblies
. You may have to restart the Microsoft Dynamics AX client after deployment, otherwise the assemblies may not get copied.
The assemblies will be deployed to a client as and when are needed. This comes down to the following three situations:
Obviously, you will want to have the assembly on your client as a developer, otherwise you will not be able to use IntelliSense or compile your code.
Now that we have created a service reference in our Visual Studio proxy library and deployed it to Microsoft Dynamics AX, we can use the types in the library from Microsoft Dynamics AX.
Let's take a look at the following X++ code that consumes the zip code service to retrieve a place name:
static void Consume_GetZipCodePlaceName(Args _args) { DynamicsAxServices.WebServices.ZipCode.USAZipCodeServiceRef .PostalCodeServiceClient postalServiceClient; DynamicsAxServices.WebServices.ZipCode.USAZipCodeServiceRef .PostalCode postalCode; System.Exception Exception; try { // Create a service client proxy postalServiceClient = new DynamicsAxServices .WebServices.ZipCode.USAZipCodeServiceRef .PostalCodeServiceClient(); // Use the zipcode to find a place name postalCode = postalServiceClient. GetPostCodeDetailByPostCode("10001"); // 10001 is New York // Use the getAnyTypeForObject to marshal the System.String to an Ax anyType // so that it can be used with info() info(strFmt('%1', CLRInterop:: getAnyTypeForObject(postalCode.get_PlaceName()))); } catch { // Get the .NET Type Exception exception = CLRInterop::getLastException(); // Go through the inner exceptions while(exception) { // Print the exception to the infolog info(CLRInterop:: getAnyTypeForObject(exception.ToString())); // Get the inner exception for more details exception = exception.get_InnerException(); } } }
When we go through the code bit by bit, we can see that a proxy client is created first. Note that this is the managed type that is created by the SvcUtil
tool when adding the service reference:
postalServiceClient = new DynamicsAxServices .WebServices.ZipCode.USAZipCodeServiceRef .PostalCodeServiceClient();
After that, using the following code, we immediately invoke the service operation with a zip code:
postalCode = postalServiceClient. GetPostCodeDetailByPostCode("10001"); // 10001 is New York
Then, there is a simple infolog message that shows the place name using the following code:
info(strFmt('%1', CLRInterop:: getAnyTypeForObject(postalCode.get_PlaceName())));
Notice the CLRInterop::getAnyTypeForObject
method, which is used to marshal between the .NET type System.String
and the X++ anyType
type before submitting it to the infolog.
That's it for consuming services. However, we also have some exception handling that handles any .NET exceptions while invoking the external service, as shown in the following code snippet:
catch { // Get the .NET Type Exception exception = CLRInterop::getLastException(); // Go through the inner exceptions while(exception) { // Print the exception to the infolog info(CLRInterop:: getAnyTypeForObject(exception.ToString())); // Get the inner exception for more details exception = exception.get_InnerException(); } }
Although the preceding code example should suffice, you will get an error message when running it. The error message is shown in the following screenshot:
What is going on here is that the service is trying to look for the endpoint's configuration in the application's configuration file but does not find it. This is because Microsoft Dynamics AX is acting as the host application here (Ax32.exe
). Therefore, the service tries to open the Ax32.exe.config
file and look for the endpoint configuration.
It is clear that putting the configuration details of every service that we want to consume into the Ax32.exe.config
file is a bit impractical and should be avoided. The solution to this issue is using the AifUtil
class to create the service client.
Let's change the preceding code so that it uses the AifUtil
class to point to the right configuration file and see what happens then. Start off by declaring a new variable of the System.Type
type at the top of the job, as shown in the following code:
System.Type type;
Take a look at the following line of code:
postalServiceClient = new DynamicsAxServices .WebServices.ZipCode.USAZipCodeServiceRef .PostalCodeServiceClient();
Replace the preceding code with the following two lines of code that use the variable that you just declared:
type = CLRInterop::getType('DynamicsAxServices.WebServices.ZipCode.USAZipCodeServiceRef.PostalCodeServiceClient'), postalServiceClient = AifUtil::createServiceClient(type);
The first line will resolve the .NET type of the service client and pass it to the AifUtil::createServiceClient
method. The AifUtil
class will then resolve the right configuration file by looking into the VSAssemblies
folder for the assembly that contains the specified type. You can see the code of the AifUtil
class's createServiceClient
method in the following code snippet:
vsAssembliesPath = xApplication::getVSAssembliesPath(); configFilePath = Microsoft.Dynamics.IntegrationFramework.ServiceReference::GetConfigFilePath(serviceClientType, vsAssembliesPath); serviceClient = Microsoft.Dynamics.IntegrationFramework.ServiceReference::CreateServiceClient(serviceClientType, configFilePath);
When you test these changes, the service should be called correctly and should give you an infolog message that shows New York as the place name.
Although the previous code consumes the external service just fine, there is another impractical issue going on when you want to deploy the code across environments.
Suppose that you want to have different versions of your service running on development, test, and production systems. Then, you will probably have three different addresses for each environment. However, the issue here is that you only have one address available in the proxy class library.
To solve this issue, we need to update our X++ code one more time. Start by declaring two new variables that will hold the endpoint and endpoint address:
System.ServiceModel.Description.ServiceEndpoint endPoint; System.ServiceModel.EndpointAddress endPointAddress;
You may have to add a reference to the System.ServiceModel
assembly to the AOT. To do that, go to the AOT, right-click on the References node, and then click on Add Reference. Next, select System.ServiceModel in the grid, click on Select, and finally, click on OK.
Then, add the following three lines of code just before the line that invokes the service operation:
endPointAddress = new System.ServiceModel.EndpointAddress ("http://www.restfulwebservices.net/wcf/USAZipCodeService.svc"); endPoint = postalServiceClient.get_Endpoint(); endPoint.set_Address(endPointAddress);
What the preceding code does is that it creates an endpoint address for the service client that is to be used. When the endpoint is created, it replaces the endpoint address that is currently being used by the service client. Note that in the preceding example, the address should be replaced by a parameter that is stored in the system. This way, you can set the endpoint address depending on the parameter value of that environment.
After all these changes, the code that consumes the services will look as follows:
static void Consume_GetZipCodePlaceNameWithEndPoint(Args _args) { DynamicsAxServices.WebServices.ZipCode.USAZipCodeServiceRef.PostalCodeServiceClient postalServiceClient; DynamicsAxServices.WebServices.ZipCode.USAZipCodeServiceRef .PostalCode postalCode; System.ServiceModel.Description.ServiceEndpoint endPoint; System.ServiceModel.EndpointAddress endPointAddress; System.Exception exception; System.Type type; try { // Get the .NET type of the client proxy type = CLRInterop::getType('DynamicsAxServices.WebServices.ZipCode.USAZipCodeServiceRef.PostalCodeServiceClient'), // Let AifUtil create the proxy client because // it uses the VSAssemblies path for the config file postalServiceClient = AifUtil::createServiceClient(type); // Create an endpoint address; this should be a // parameter stored in the system endPointAddress = new System.ServiceModel.EndpointAddress("http://www.restfulwebservices.net/wcf/USAZipCodeService.svc"); // Get the WCF endpoint endPoint = postalServiceClient.get_Endpoint(); // Set the endpoint address. endPoint.set_Address(endPointAddress); // Use the zipcode to find a place name postalCode = postalServiceClient.GetPostCodeDetailByPostCode("10001"); // 10001 is New York // Use the getAnyTypeForObject to marshal the // System.String to an Ax anyType // so that it can be used with info() info(strFmt('%1', CLRInterop::getAnyTypeForObject(postalCode.get_PlaceName()))); } catch { // Get the .NET Type Exception exception = CLRInterop::getLastException(); // Go through the inner exceptions while(exception) { // Print the exception to the infolog info(CLRInterop::getAnyTypeForObject(exception.ToString())); // Get the inner exception for more details exception = exception.get_InnerException(); } } }
3.135.247.11