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:-)
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…
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;
}
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 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.