Sunday, October 20, 2013

Microsoft Dynamics AX 2012 – Developing Apps [Not Secure]

Microsoft Dynamics AX 2012 – Developing Apps [Not Secure]
 
Purpose: The purpose of this document is to illustrate how to integrate Microsoft Dynamics AX 2012 with external applications and explain different approaches to authentication and development.
 
Challenge: For the purposes of demonstration or Proof of Concept it may be needed to quickly develop external application which is integrated with Microsoft Dynamics AX 2012. There're also multiple development approaches you may want to choose and multiple authentication strategies you may want to use. 

Solution: For the purposes of this walkthrough I'll use Microsoft Dynamics AX 2012 R2 deployed in Windows Azure and Microsoft Dynamics AX 2012 R2 Demo VM which can be downloaded from Customer/Partner Source. I'm going to develop external application which will integrate with Microsoft Dynamics AX 2012 using Document and Custom Web Service. Also I'll illustrate the use of Windows authentication and Claims-based authentication in integration scenarios. Please note that in this walkthrough I'm focusing on developing apps for POC [Not Secure], please refer to another article in this series about Developing Apps [Secure] leveraging Windows Azure Service Bus.

Task: Console Application which I'll develop in this walkthrough will display the info about Cases retrieved from Microsoft Dynamics AX 2012. I used Console Application for the sake of simplicity and this material will also be applicable in case you develop Windows 8, Windows Phone8 or Web apps integrated with Microsoft Dynamics AX 2012.

Walkthrough:
First off I'll deploy Microsoft Dynamics AX 2012 in Windows Azure by creating Windows Azure VM(s)
 
Endpoints
 
 
And exposing HTTP/HTTPS endpoint for the VM in order to establish connection to Microsoft Dynamics AX 2012 from external application [Not Secure]
 
Add Endpoint – Add an endpoint to a virtual machine
 
 
Once we chose Add standalone endpoint we can now select endpoint name HTTP
 
Add Endpoint – Specify the details of the endpoint
 
 
As the result HTTP endpoint will be exposed for the VM
 
Endpoints
 
 
Now we have to install Web Server Role on the VM in order to expose Web Services through HTTP/HTTPS
 
Add Roles – Web Server (IIS)
 
 
Add Roles – Web Server (IIS)
 
 
Add Roles – Web Server (IIS)
 
 
After successful installation of Web Server (IIS) Role we can successfully access the following URL from the local machine: http://azureax.cloudapp.net/
 
IIS - Port 80
 
 
The next step is to install Web Services on IIS component for Microsoft Dynamics AX 2012
 
Microsoft Dynamics AX 2012 - Web Services on IIS
 
 
During the installation you may encounter the following error which means that not all prerequisites have been installed on the machine before installation of Web Services on IIS
 
Prerequisites details
 
 
In my case the problem was caused by the absence of Health and Diagnostics – Request Monitor component which is not installed by default with Web Server (IIS) Role
 
Add Roles – Web Server (IIS)
 
 
That's why I simply added Health and Diagnostics – Request Monitor component on the VM in order to avoid installation error
 
Now we can continue with Web Services on IIS installation process
 
The next step would be to specify Business Connector Proxy account information
 
Microsoft Dynamics AX Setup – Specify Business Connector Proxy account information
 
 
After that we have to specify hosting Web site details as depicted below
 
Microsoft Dynamics AX 2012 Setup – Configure IIS for Web Services
 
 
And before we complete installation we will specify AOS account
 
Microsoft Dynamics AX 2012 Setup – Specify an AOS account
 
 
After you install Web Server (IIS) Role you may also need to configure it. Please refer to the following article about how to Configure IIS: http://technet.microsoft.com/en-us/library/gg731848.aspx
 
By now we completed required installations and can think about authentication. For the sake of simplicity I'll enable Windows Authentication for Web Server (IIS) on Windows Azure VM
 
Add Roles – Web Server (IIS)
 
 
Please note that Windows authentication is not appropriate for use in an Internet environment because that environment does not require or encrypt user credentials. Please refer to the following article about Configuring Windows authentication on IIS: http://technet.microsoft.com/en-us/library/cc754628(v=ws.10).aspx
 
However for the purposes of POC this is the quickest way for me to authenticate. That's why now I'll check that Windows authentication is enabled for Default Web Site > MicrosoftDynamicsAXAif60 in IIS Manager  
 
IIS Manager
 
 
In Microsoft Dynamics AX 2012 I'll validate Web Site URL
 
Microsoft Dynamics AX 2012 – Web Sites (Validate)
 
 
Microsoft Dynamics AX 2012 – Web Sites (Validate Result)
 
 
And then create Enhanced Inbound port
 
Enhanced port / HTTP
 
 
To expose standard CaseWebDetailService Document Web Service in Microsoft Dynamics AX 2012
 
Select Service Operations
 
 
CaseWebDetailService Document Service will allow me to retrieve info about Cases in Microsoft Dynamics AX 2012 from external application
 
Document Data Policies 
 
 
Once Enhanced Inbound port has been activated I can now access respective WSDL URI using the following URL: http://azureax/MicrosoftDynamicsAXAif60/CaseManagement/xppservice.svc
 
In order to access WSDL URI from external application I will use the full machine name as: http://azureax.cloudapp.net/MicrosoftDynamicsAXAif60/CaseManagement/xppservice.svc
 
Here's how WSDL URI looks like
 
RoutingService Service
 
 
RoutingService Service
 
RoutingService Service
 
You have created a service.
To test this service, you will need to create a client and use it to call the service. You can do this using the svcutil.exe tool from the command line with the following syntax:
 
You can also access the service description as a single file:
This will generate a configuration file and a code file that contains the client class. Add the two files to your client application and use the generated client class to call the Service. For example:
C#
class Test
{
    static void Main()
    {
        RequestReplyRouterClient client = new RequestReplyRouterClient();
 
        // Use the 'client' variable to call operations on the service.
 
        // Always close the client.
        client.Close();
    }
}
 
Visual Basic
Class Test
    Shared Sub Main()
        Dim client As RequestReplyRouterClient = New RequestReplyRouterClient()
        ' Use the 'client' variable to call operations on the service.
 
        ' Always close the client.
        client.Close()
    End Sub
End Class
 
Now let's develop a simple client application (Console application) to retrieve the info about Cases from Microsoft Dynamics AX 2012
 
For these purpose I quickly installed Visual Studio Express 2012 for Windows Desktop on my local machine and on Windows Azure VM from here: http://www.microsoft.com/visualstudio/eng/downloads. After that I'm going to create a project
 
New Project
 
 
And add Service Reference using Web Service WSDL URI
 
Add Service Reference
 
 
Once Service Reference has been added all necessary Proxy classes will be generated inside the project
 
Proxy Classes
 
 
Please note that CaseWebDetailService Web Service is a Document Web Service Microsoft Dynamics AX 2012, that's why the system generated all necessary AIF classes (Axd, etc)
 
The source code for client application is presented below
 
Source code
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApplication1.ServiceReference1;
 
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {          
            try
            {
                CaseWebDetailServiceClient client = new CaseWebDetailServiceClient();
 
                client.ClientCredentials.Windows.ClientCredential.Domain = "azureax0";
                client.ClientCredentials.Windows.ClientCredential.UserName = "Administrator";
                client.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";
 
                CallContext context = new CallContext();
                context.Company = "DAT";
 
                EntityKey[] entityKey = new EntityKey[1];
                entityKey[0] = new EntityKey();
                entityKey[0].KeyData = new KeyField[1];
                entityKey[0].KeyData[0] = new KeyField();
                entityKey[0].KeyData[0].Field = "RecId";
                entityKey[0].KeyData[0].Value = "5637144576";
 
                AxdCaseWebDetail caseItem = client.read(context, entityKey);
 
                if (caseItem != null)
                {
                    Console.WriteLine(String.Format("CaseId: {0}; Description: {1}", caseItem.CaseDetail[0].CaseId, caseItem.CaseDetail[0].Description));
                }
 
                Console.WriteLine("Done!");
                Console.ReadLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error!" + ex.Message);
                Console.ReadLine();
            }
        }
    }
}
 
Please note that for the sake of simplicity I hardcoded user credentials. I'll also need to change endpoint address in App.config appropriately from "azureax.azureax0.local" to "azureax.cloudapp.net" to be able to resolve the name of Windows Azure VM from my local machine where I create my client application
 
App.config
 
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="reqReplyEndpoint">
                    <security mode="TransportCredentialOnly">
                        <transport clientCredentialType="Windows" />
                    </security>
                </binding>
                <binding name="BasicHttpBinding_CaseWebDetailService">
                    <security mode="TransportCredentialOnly">
                        <transport clientCredentialType="Windows" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://azureax.cloudapp.net/MicrosoftDynamicsAXAif60/CaseManagement/xppservice.svc"
                binding="basicHttpBinding" bindingConfiguration="reqReplyEndpoint"
                contract="ServiceReference1.IRequestReplyRouter" name="reqReplyEndpoint" />
            <endpoint address="http://azureax.cloudapp.net/MicrosoftDynamicsAXAif60/CaseManagement/xppservice.svc"
                binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_CaseWebDetailService"
                contract="ServiceReference1.CaseWebDetailService" name="BasicHttpBinding_CaseWebDetailService" />
        </client>
    </system.serviceModel>
</configuration>
 
As the result I'll be able to retrieve info about Cases from Microsoft Dynamics AX 2012 in my client application which I ran from my local machine as well as from the same Windows Azure VM
 
Microsoft Dynamics AX 2012 - Cases
 
 
Microsoft Dynamics AX 2012 – Table Browser
 
 
As you can see I ran client application from my local machine as well as from the same Windows Azure VM
 
Result - Azure VM
 
 
Result - Local machine
 
 
Okay, we got the result! However there's another way to authenticate against Microsoft Dynamics AX 2012 using AIF Intermediary proxy. The idea is very simple, you can use AIF Intermediary proxy in the scenarios when your external application has already taken care on authentication (using Claims-based authentication), so when you authenticate against Microsoft Dynamics AX 2012 you simply use a static integration account credentials (impersonation of AX integration user) also adding logon user information (email address which is already linked to AX integration user)  
 
For example, I say that alex@outlook.com is a Claims user which is linked to AX integration user Admin. Please see below the setup required for this scenario
 
First I will create AX Claims user external (alex@outlook.com)
 
User – Claims user
 
 
Then I will have to specify the association between AX Claims user and AX integration user in Trusted intermediary users form for Inbound port
 
Inbound port – Security (Trusted Intermediary users)
 
 
Please note that before you define the association you have to mark "Allow trusted intermediary to impersonate" checkbox [V] for Inbound port
 
Trusted intermediaries
 
 
Please note that I associated alex@outlook.com Claims user with trusted intermediary user Admin in Microsoft Dynamics AX 2012
 
Now I'll change the source code little bit to call Web Services on behalf of external user
 
Source code
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApplication1.ServiceReference1;
 
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {          
            try
            {
                CaseWebDetailServiceClient client = new CaseWebDetailServiceClient();
 
                client.ClientCredentials.Windows.ClientCredential.Domain = "azureax0";
                client.ClientCredentials.Windows.ClientCredential.UserName = "Administrator";
                client.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";               
 
                CallContext context = new CallContext();
                context.Company = "DAT";
                context.LogonAsUser = "external\\alex@outlook.com";
 
                EntityKey[] entityKey = new EntityKey[1];
                entityKey[0] = new EntityKey();
                entityKey[0].KeyData = new KeyField[1];
                entityKey[0].KeyData[0] = new KeyField();
                entityKey[0].KeyData[0].Field = "RecId";
                entityKey[0].KeyData[0].Value = "5637144576";
 
                AxdCaseWebDetail caseItem = client.read(context, entityKey);
 
                if (caseItem != null)
                {
                    Console.WriteLine(String.Format("CaseId: {0}; Description: {1}", caseItem.CaseDetail[0].CaseId, caseItem.CaseDetail[0].Description));
                }
 
                Console.WriteLine("Done!");
                Console.ReadLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error!" + ex.Message);
                Console.ReadLine();
            }
        }
    }
}
 
Please note that if you call this code as System Administrator you can successfully get the info about Case from Microsoft Dynamics AX 2012
 
 
However if you call this code NOT as System Administrator your Web Service call will fail with authentication error
 
 
By now we discussed how to use standard Document Service to retrieve information about Cases in Microsoft Dynamics AX 2012. Document Services in Microsoft Dynamics AX 2012 use AIF infrastructure/framework, that's why after we added corresponding Service Reference the system generated a number of AIF Proxy classes for us (Axd, etc)
 
Please note that you can also apply "pure WCF" approach by using Custom Services in Microsoft Dynamics AX 2012 when you don't use AIF, and define Data Contract and Service Contract in X++. Please find the reference on how to use Custom Services in Microsoft Dynamics AX 2012 here: http://technet.microsoft.com/en-us/library/hh509052.aspx
 
For Custom Service scenario I'll use local Microsoft Dynamics AX 2012 R2 Demo VM
Please see below how we can define Data Contact and Service Contract for Custom Web Service which will help us retrieve information about Cases in Microsoft Dynamics AX 2012 
 
Source code – Data Contract Class
 
[DataContractAttribute]
public class AlexCase
{
    str alexCaseId;
    str alexCaseDescription;
}
[DataMemberAttribute]
public str caseId(str _alexCaseId = alexCaseId)
{
    alexCaseId = _alexCaseId;
    return alexCaseId;
}
[DataMemberAttribute]
public str description(str _alexCaseDescription = alexCaseDescription)
{
    alexCaseDescription = _alexCaseDescription;
    return alexCaseDescription;
}
 
Please note that in Data Contract I defined 2 fields (Id and Name) for the sake of simplicity
 
Source code – Service Contract Class
 
class AlexCaseService
{
}
[SysEntryPointAttribute(true)]
public AlexCase getCase()
{
    AlexCase alexCase;
    ;
 
    alexCase = new AlexCase();
    alexCase.caseId('1');
    alexCase.description('AlexCase');
   
    return alexCase;
}
 
Please note that in Service Contract Class I implemented just one operation to retrieve Case info for the sake of simplicity
 
Once Data Contract and Service Contract have been implemented we can create Service, associate Service Contract Class to newly created Service, add Service to Service Group
 
Project
 
 
And finally deploy Service group which will create and activate Inbound port
 
Inbound port
 
 
Please note that I created Enhanced Inbound port using HTTP adapter to expose Custom Service
 
Select Service Operations
 
 
Please note that I selected getCase operation from the list to finalize Inbound port setup
Now I can create a simple client application to consume Custom Service I exposed, so I'll go ahead create a project and add Service Reference using WSDL URI
 
Add Service Reference
 
 
Now let's take a look at the list of proxy classes
 
Proxy Classes
 
 
In this walkthrough I implemented two options of how to call Custom Service: Synchronous and Asynchronous
Please see the source code for Synchronous scenario below
 
Source code - Sync
 
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)
        {
            AlexCaseServiceClient client = new AlexCaseServiceClient();
 
            client.ClientCredentials.Windows.ClientCredential.Domain = "contoso";
            client.ClientCredentials.Windows.ClientCredential.UserName = "Administrator";
            client.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";
 
            CallContext context = new CallContext();
            context.Company = "USMF";
 
            try
            {
                AlexCase alexCase = client.getCase(context);
 
                if (alexCase != null)
                {
                    Console.WriteLine(alexCase.caseId + ":" + alexCase.description);
                }
 
                Console.WriteLine("Done!");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.InnerException.Message);
                Console.WriteLine("Error!");
            }
 
            Console.ReadLine();
        }
    }
}
 
Please see the source code for Asynchronous scenario below
 
Source code – Async
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ConsoleApplication2.ServiceReference1;
 
namespace ConsoleApplication2
{
    class Program
    {
        public static async Task DoAsync()
        {
            AlexCaseServiceClient client = new AlexCaseServiceClient();
 
            client.ClientCredentials.Windows.ClientCredential.Domain = "contoso";
            client.ClientCredentials.Windows.ClientCredential.UserName = "Administrator";
            client.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";
 
            CallContext context = new CallContext();
            context.Company = "USMF";
 
            try
            {
                AlexCaseServiceGetCaseResponse result = await client.getCaseAsync(context);
 
                if (result != null)
                {
                    Console.WriteLine(result.response.caseId + ":" + result.response.description);
                }
 
                Console.WriteLine("Success");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.InnerException.Message);
                Console.WriteLine("Failure");
            }
        }
 
        static void Main(string[] args)
        {
            var Task = DoAsync();
 
            Console.ReadLine();
        }     
    }
}
 
Before I will execute this code I have to come back to authentication setup and make sure Windows Authentication is enabled for IIS Web Site
 
IIS Web site Authentication settings
 
 
I also had to change ClientCredentialsType = Windows to ClientCredentialType = Ntml in App.config to avoid the following error: The remote server returned an error: (401) Unauthorized. The HTTP request is unauthorized with client authentication scheme 'Negotiate'.
 
App config
 
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="serviceEndpoint">
                    <security mode="TransportCredentialOnly">
                        <transport clientCredentialType="Ntlm" />
                    </security>
                </binding>
                <binding name="BasicHttpBinding_AlexCaseService">
                    <security mode="TransportCredentialOnly">
                        <transport clientCredentialType="Ntlm" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://ax2012r2a.contoso.com/MicrosoftDynamicsAXAif60/AlexServiceExt/xppservice.svc"
                binding="basicHttpBinding" bindingConfiguration="serviceEndpoint"
                contract="ServiceReference1.IRequestReplyRouter" name="serviceEndpoint" />
            <endpoint address="http://ax2012r2a.contoso.com/MicrosoftDynamicsAXAif60/AlexServiceExt/xppservice.svc"
                binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_AlexCaseService"
                contract="ServiceReference1.AlexCaseService" name="BasicHttpBinding_AlexCaseService" />
        </client>
    </system.serviceModel>
</configuration>
 
As the result I can successfully retrieve info about Case in Microsoft Dynamics AX 2012 from external application using Custom Service, just the same way I did this using Document Service
Please note that Custom Services in Microsoft Dynamics AX 2012 can offer you greater flexibility and performance, development process for Custom Services resembles the way you develop for .NET/WCF and Custom Services are not dependent of AIF infrastructure/framework   
 
 
Summary: In this walkthrough I demonstrated how to quickly develop external application which is integrated with Microsoft Dynamics AX 2012 using Document Services and Custom Services. Please note that in this walkthrough for the sake of simplicity I used impersonation and Windows Integrated authentication [Not Secure] as well as outlined other authentication scenarios such as AIF Intermediary proxies. I provided a comparison Document Services and Custom Services in Microsoft Dynamics AX 2012 and outlined some benefits of Custom Services which may offer greater flexibility (.NET like, sync/async calls) and performance (WCF).  

Author:
Alex Anikiev, PhD, MCP

Tags: Microsoft Dynamics ERP, Microsoft Dynamics AX 2012, Visual Studio 2012, AIF, Application Integration Framework, Web Services, Document Services, Custom Services, .NET, WCF, Synchronous, Asynchronous

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.