Philippsen's Blog

Everyday findings in my world of .net and related stuff

Posts Tagged ‘Dynamics AX 2009’

Wrapping a webservice in a .net assembly for Dynamics AX

Posted by Torben M. Philippsen on July 4, 2012

As for Dyanamics AX 2009 I quite often bump into problems when trying to consume external webservices inside Dynamics AX.

The problems that I’ve experienced often relates either to the WSDL causing AX to throw an CLR error when trying to add a service reference or if the webservice requires authentication or similar, which is not possible to configure/set within AX.

The workaround that has shown to come in handy numerous times, is to create a webservice wrapper in .net and having that wrapper added as a reference in the AOT.
In order to be able to perform this task, You will need to do as follows:

  1. Create a new library project in Visual Studio
  2. Add a service reference to you WSDL file. In “advanced settings” make sure that the “Access level for generated classes” is set to “public”.
  3. compile the DLL
  4. Copy the dll to the client bin folder.
    This procedure is primarily only necessary in order to being able to select the assembly from the AOT or if You want to be able to use the assembly from AX clients.
  5. Copy and rename the corresponding *.config file as “ax32.exe.config”. If the file already exist, merge the content of Your file with the existing one.
  6. Copy the dll to the AX server (AOS) bin folder.
  7. Copy and rename the corresponding *.config file as “ax32serv.exe.config”. If the file already exist, merge the content of Your file with the existing one.
  8. test using the dll from client side jobs. you can leave this out, if You’re only going to use the assmebly from server side operations.
  9. test using the dll from server side classes.

Ofcource there is always pros and cons when dealing with workarounds…

Cons:

  • a slight increase in complexity when dealing with components outside AX.
  • You need to remember to copy the dll to new AOS instances if you in the future are adding new ones to Your setup.
  • some people don’t like to have code residing outside AX

Pros:

  • The abilty to access proxy properties that aren’t normally exposed to AX – eg. “ClientCredentials”.
  • when a wsdl fails to be added as a service reference in AX you will most likely not experience the same problems from .net. This means that You could either try to have the webservice changed into something more AX friendly (good luck with that) or you could just simply choose/be forced to use this workaround;-)

Posted in Microsoft Dynamics AX, Webservices | Tagged: , , | Comments Off on Wrapping a webservice in a .net assembly for Dynamics AX

Digging into AIF

Posted by Torben M. Philippsen on November 29, 2011

Given my experiance in WCF I recently had the pleasure to dig into AIF (the Application Integration Framework in Dynamics AX).

As a newbie I quickly had to learn my lessons – and to avoid having to learn the same lessons twice I decided to add this post:-)

Initially I had to realize that these questions needed an answer:

  • Inside Dynmics AX – what is an endpoint?
  • Inside AX – what’s the idea of having local endpoints along with endpoints – what’s the difference?
  • How do I target a specific company?
  • How do I configure my .net client to use a certain AX endpoint?
  • How do I make sure that a certain datapolicy is actually applied to incoming messages?

In my examples I will use the build in LedgerPurchaseInvoiceService.

Asuming that this service has been enabled in “basic -> setup -> Application Integration Framework -> services” go to “basic -> setup -> Application Integration Framework -> endpoints”. There You will at least find the default Endpoint. AX uses the “Default Endpoint” as the destination endpoint and the local endpoint of the user calling the service for processing WCF requests. I wanted to be able to specify which endpoints my requests were using in order to take advantage of the security and processing features in AIF, This means that if no endpoint is specified from the client side when sending messages through AIF, the default endpoint is used. This also means that if You configure Your own endpoints these need to be specified from the client side…

We need to answer some of the questions above:

  1. What is an endpoint?
    Well I will try to explain this in my own terms… An endpoint is the single point of entry that is used from the client side. An endpoint is associated to a company which means if you configure an endpoint in company “A” you won’t be able to use that endpoint from comapny “B”.
    The endpoint is where you define the datapolicies – which fields are enabled, which fields are required and so on…
    An endpoint is based on a local endpoint.
  2. What’s the idea of having local endpoints?
    Local endpoints are global:-) This means that any of the local endpoints are visible through out all companies. A local endpoint is used to point to a certain company. Which brings us to question number 3
  3. If I want to target a certain company from the client side I will have to point to an endpoint configured in that company that uses a local endpoint associated to the same company.
    endpoints

    endpoints

    local_endpoints

    local_endpoints

    In my case I want to use the “SignFlow” endpoint from the client – which brings us to question number 4

  4. How do I configure my .net client to use a certain AX endpoint?
    The  magic word is “SOAP Headers”. Before calling any of the functions of your client object. You need to specify the destination endpoint, the sourceEndpoint and the sourceEndpointUser. The last two headers requires that You use a binding type that supports WS-Addressing.
    To add the SOAP Header You do the following:
 			PurchaseInvoiceServiceClient client = new PurchaseInvoiceServiceClient ();
 			try
 			{
 				//SOAP header info
 				using (new OperationContextScope (client.InnerChannel))
 				{
 					//CREATE HEADER TO SET SOURCE ENDPOINT
 					//this assumes that a endpoint (inside AX) with the name [SignFlow] has been created for all companies
 					SetDestinationEndpoint(ddlDataAraeId.SelectedValue);
 					//CREATE HEADER TO SET TARGET COMPANY
 					//this assumes that a local endpoint (inside ax) with the name [ddlDataAraeId.SelectedValue] has been configured
 					//and is associated with a company that exists in dynamics ax
 					SetSourceEndpointAndUser("SignFlow" );
 					//submit the request and retrieve the respons 
 					keys = client.create(recordWrapper);
 				}
 			}

 		/// <summary>
 		/// Helper method - adds a SOAP Header defining the destination endpoint (local endpoint) in Dynamics AX
 		/// </summary>
 		/// <param name="nameOfEndpoint">The name of the local endpoint</param>
 		private void SetDestinationEndpoint(string nameOfEndpoint)
 		{
 				OperationContext .Current.OutgoingMessageHeaders.Add(MessageHeader .CreateHeader("DestinationEndpoint" , "http://schemas.microsoft.com/dynamics/2008/01/services" , nameOfEndpoint)); 
 		}

 		/// <summary>
 		/// Helper method - adds a SOAP Header defining the source endpoint name and the source endpoint user to use
 		/// </summary>
 		/// <param name="sourceEndpointName">the name of the source endpoint</param>
 		private void SetSourceEndpointAndUser(string sourceEndpointName)
 		{
 			string userName = HttpContext .Current.User.Identity.Name.ToString(); //returns the current user and domian - eg. domainname\\username
 			var addressHeader = AddressHeader .CreateAddressHeader("SourceEndpointUser" , "http://schemas.microsoft.com/dynamics/2008/01/services" , userName);
 			var addressBuilder = new EndpointAddressBuilder (
 			new EndpointAddress (new Uri ("urn:" + sourceEndpointName), addressHeader));
 			var endpointAddress = addressBuilder.ToEndpointAddress();
 			OperationContext .Current.OutgoingMessageHeaders.From = endpointAddress;
 		}

The entire client code looks like this:

 using System;
 using System.Web;
 using System.Web.UI;
 using System.ServiceModel;
 using System.ServiceModel.Channels;
 using SignFlowIntegration.EG_InitialPurchaseInvoiceReg;

 namespace SignFlowIntegration
 {
 	public partial class InitialReg : System.Web.UI.Page
 	{
 		private enum DataAreaIdCollection { SVE, DMO, DAT, LON, MAS, FIN };
 		private enum CurrencyCodeCollection { DKK, AUD, CHF, CAD, EUR, GBP, NOK, SEK, USD, PLN };
 		protected void Page_Load(object sender, EventArgs e)
 		{
 			if (!Page.IsPostBack)
 			{
 				ddlDataAraeId.DataSource = System.Enum .GetValues(typeof (DataAreaIdCollection ));
 				ddlCurrencyCode.DataSource = System.Enum .GetValues(typeof (CurrencyCodeCollection ));
 				ddlDataAraeId.DataBind();
 				ddlCurrencyCode.DataBind();
 			}
 		}

 		/// <summary>
 		/// Handles the submit button click event
 		/// </summary>
 		/// <param name="sender"></param>
 		/// <param name="e"></param>
 		protected void btnSubmit_Click(object sender, EventArgs e)
 		{
 			decimal amountCur;
 			DateTime documentDate;
 			//create the client
 			PurchaseInvoiceServiceClient client = new PurchaseInvoiceServiceClient ();
 			//create wrapper for record object
 			AxdPurchaseInvoice recordWrapper = new AxdPurchaseInvoice ();
 			//create record objects
 			AxdEntity_LedgerJournalTable ledgerJournalTableObj = new AxdEntity_LedgerJournalTable ();
 			AxdEntity_LedgerJournalTrans ledgerJournalTransObj = new AxdEntity_LedgerJournalTrans (); 
 			//-----------------------------------------
 			//step 1
 			//-----------------------------------------
 			//assign values to record objects
 			ledgerJournalTableObj.JournalName = txtJournalName.Text;
 			ledgerJournalTableObj.JournalTypeSpecified = true ;
 			ledgerJournalTableObj.JournalType = AxdEnum_LedgerJournalType .PurchaseLedger;
 			ledgerJournalTransObj.AccountNum = txtAccountNum.Text;
 			ledgerJournalTransObj.EG_SignFlowCaseId = txtSignFlowsagsnr.Text;
 			if (DateTime .TryParse(txtDocumentDate.Text, out documentDate))
 			{
 				//set documentdate
 				ledgerJournalTransObj.DocumentDate = documentDate;
 			}
 			else
 			{
 				//default
 				ledgerJournalTransObj.DocumentDate = DateTime .Now;
 			}
 			ledgerJournalTransObj.CurrencyCode = ddlCurrencyCode.SelectedValue;
 			ledgerJournalTransObj.ApprovedBy = txtApprovedBy.Text;
 			ledgerJournalTransObj.Invoice = txtInvoice.Text;
 			if (decimal .TryParse(txtAmountCurCredit.Text, out amountCur))
 			{
 				ledgerJournalTransObj.AmountCurCreditSpecified = true ;
 				ledgerJournalTransObj.AmountCurCredit = amountCur;
 			}
 			else if (decimal .TryParse(txtAmountCurDebit.Text, out amountCur))
 			{
 				ledgerJournalTransObj.AmountCurDebitSpecified = true ;
 				ledgerJournalTransObj.AmountCurDebit = amountCur;
 			}
 			else
 			{
 				//default
 				ledgerJournalTransObj.AmountCurCreditSpecified = true ;
 				ledgerJournalTransObj.AmountCurCredit = 0;
 			}
 			//-----------------------------
 			//step 2 - add record to wrapper
 			//-----------------------------
 			recordWrapper.LedgerJournalTable = new AxdEntity_LedgerJournalTable [1]; 
 			ledgerJournalTableObj.LedgerJournalTrans = new AxdEntity_LedgerJournalTrans [1];
 			ledgerJournalTableObj.LedgerJournalTrans[0] = ledgerJournalTransObj;
 			recordWrapper.LedgerJournalTable[0] = ledgerJournalTableObj;
 			//Set impersonation level
 			client.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel .Impersonation;
 			//prepare the repons object
 			EntityKey [] keys = new EntityKey [1];
 			//--------------------------------------
 			//step 3 - call create method on service
 			//add proper error handling
 			//--------------------------------------

 			try
 			{
 				//SOAP header info
 				using (new OperationContextScope (client.InnerChannel))
 				{
 					//CREATE HEADER TO SET SOURCE ENDPOINT
 					//this assumes that a endpoint (inside AX) with the name [SignFlow] has been created for all companies
 					SetDestinationEndpoint(ddlDataAraeId.SelectedValue);
 					//CREATE HEADER TO SET TARGET COMPANY
 					//this assumes that a local endpoint (inside ax) with the name [ddlDataAraeId.SelectedValue] has been configured
 					//and is associated with a company that exists in dynamics ax
 					SetSourceEndpointAndUser("SignFlow" );
 					//submit the request and retrieve the respons 
 					keys = client.create(recordWrapper);
 				}
 			}
 			catch (System.ServiceModel.FaultException <AifFault > aex)
 			{

 				//TODO add proper error handling
 				txtResult.Text += "AIF ERROR\\n" ;
 				txtResult.Text += "Message: " + aex.Message + "\\n" ;
 				txtResult.Text += "Source: " + aex.Source + "\\n" ;
 				txtResult.Text += "StackTrace: " + aex.StackTrace + "\\n" ;
 				txtResult.Text += "----------------------------------------\\n" ;
 				//txtResult.Text += aex.Detail.FaultMessageListArray[0].FaultMessageArray[0].Message;
 			}
 			catch (System.ServiceModel.FaultException fex)
 			{
 				//SOAP errors are handled here
 				//TODO add proper error handling
 				txtResult.Text += "Fault ERROR\\n" ;
 				txtResult.Text += "Message: " + fex.Message + "\\n" ;
 				txtResult.Text += "Source: " + fex.Source + "\\n" ;
 				txtResult.Text += "StackTrace: " + fex.StackTrace + "\\n" ;
 			}
 			catch (Exception ex)
 			{
 				//TODO add proper error handling
 				txtResult.Text += "ERROR\\n" ;
 				txtResult.Text += "Message: " + ex.Message + "\\n" ;
 				txtResult.Text += "Source: " + ex.Source + "\\n" ;
 				txtResult.Text += "StackTrace: " + ex.StackTrace + "\\n" ;
 			}
 			finally
 			{
 				//close the client
 				client.Close();
 			}

 			//----------------------------------
 			//Step 4 - Handle the response
 			//----------------------------------
 			//examine the respons - is it valid?
 			if (keys[0] != null ) // the repons contains info ->'s valid
 			{
 				txtResult.Text += "SUCCESS\\n" ;
 				KeyField [] fields = keys[0].KeyData;
 				foreach (KeyField field in fields)
 				{
 					//print the result
 					txtResult.Text += field.Field + ": " + field.Value + "\\n" ;
 				}
 			}
 		}

 		/// <summary>
 		/// Helper method - adds a SOAP Header defining the destination endpoint (local endpoint) in Dynamics AX
 		/// </summary>
 		/// <param name="nameOfEndpoint">The name of the local endpoint</param>
 		private void SetDestinationEndpoint(string nameOfEndpoint)
 		{
 				OperationContext .Current.OutgoingMessageHeaders.Add(MessageHeader .CreateHeader("DestinationEndpoint" , "http://schemas.microsoft.com/dynamics/2008/01/services" , nameOfEndpoint)); 
 		}

 		/// <summary>
 		/// Helper method - adds a SOAP Header defining the source endpoint name and the source endpoint user to use
 		/// </summary>
 		/// <param name="sourceEndpointName">the name of the source endpoint</param>
 		private void SetSourceEndpointAndUser(string sourceEndpointName)
 		{
 			string userName = HttpContext .Current.User.Identity.Name.ToString(); //returns the current user and domian - eg. egdk\\tomph
 			var addressHeader = AddressHeader .CreateAddressHeader("SourceEndpointUser" , "http://schemas.microsoft.com/dynamics/2008/01/services" , userName);
 			var addressBuilder = new EndpointAddressBuilder (
 			new EndpointAddress (new Uri ("urn:" + sourceEndpointName), addressHeader));
 			var endpointAddress = addressBuilder.ToEndpointAddress();
 			OperationContext .Current.OutgoingMessageHeaders.From = endpointAddress;
 		}

 	}

 }

Please remember that You have to use a bindign that support WS-Addressing. You can either use any of the pre defined ws bindings or use the “custom binding”. In my case I have used the custom binding, which is the most easy one to configure if You just want to test this out. Just set the Authentication property to NTLM instead of anonymous – we are not interested in anonymous user in AX.

Please keep in mind, that if Your setup allows it, You are always able to use the default endpoint, which means that all the SOAP header stuff can be left out of the equation.
Thanks to my collegue Michael Cronqvist for assisting me and also great thanks to my former collegue Tue Theilmann Jørgensen for pointing me in the SOAP Headers direction.

These aditional ressources might come in handy:

Posted in AIF, Microsoft Dynamics AX | Tagged: , , , , , , | Comments Off on Digging into AIF

 
%d bloggers like this: