In this post I'll try to demonstrate how to use AIF outbound web service adapter. I'll begin with building a sample web service implementing AIF SendMessage "interface". We will validate the web service by building a test.
For the end-to-end test we will need to deploy the web service to a real IIS server. When IIS is configured and web serviced deployed we will configure AIF in Dynamics AX 4.0.
At the end we will run our end-to-end test by sending purchase requisition to our web service.
Let's begin with building our new web service. I assume you have Microsoft VisualStudio 2005 and NUnit (you can use Team System test framework or any other unit test framework but I picked NUnit because I believe that everybody has access to this one) ready on your box.
[WebService(Namespace = "http://localhost/AifWebServiceTest/MyWebService.asmx/")]
using System; using System.Collections.Generic; using System.Text; using NUnit.Framework; namespace AifWebServiceTest { [TestFixture] public class MyWebServiceTest { private MyWebService.MyWebService service; [Test] public void TestHelloMessage() { Assert.AreEqual("Hello World", service.GetTestMessage()); } [SetUp] public void SetUp() { service = new MyWebService.MyWebService(); } [TearDown] public void TearDown() { service = null; } } }
Let's update the unit test to capture requirements for our web service. Our web service will store all messages sent in a cache. Later you can ask the web service to retrieve all messages sent to it.
Add the following tests to your unit test (notice that Array2String is a helper method converting array of string to a string representation of that array):
This method clears the cache:
[Test] public void TestClearMessages() { string message = service.GetTestMessage(); Assert.AreEqual(0, service.GetMessages().Length); service.SendMessage(message); Assert.AreEqual(1, service.GetMessages().Length); //ClearMessages: clears the cache service.ClearMessages(); Assert.AreEqual(0, service.GetMessages().Length); }
This method returns all messages from the cache:
[Test] public void TestGetMessages() { string message = service.GetTestMessage(); List< string > emptyList = new List< string >(); Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages())); service.SendMessage(message); emptyList.Add(message); service.SendMessage(message); emptyList.Add(message); //GetMessages: retrieves an array of messages from the cache Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages())); }
This method sends a message to the cache:
[Test] public void TestSendMessage() { string message = service.GetTestMessage(); List< string > emptyList = new List< string >(); Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages())); //SendMessage: add string ( = input ) to the cache service.SendMessage(message); emptyList.Add(message); Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages())); service.SendMessage(message); emptyList.Add(message); Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages())); }
We will also need this method for our AIF end-to-end scenario:
[Test] public void TestReadPurchaseRequisition() { string message = service.GetTestMessage(); List< string > emptyList = new List< string >(); Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages())); //SendMessage: add string ( = input ) to the cache service.readPurchaseRequisition(message); emptyList.Add(message); Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages())); service.readPurchaseRequisition(message); emptyList.Add(message); Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages())); }
Let's code the helper method:
private string Array2String(string[] array) { StringBuilder content = new StringBuilder(); content.Append("<"); foreach (string value in array) { if (content.Length > 1) content.Append(", "); content.AppendFormat("\"{0}\"", value); } content.Append(">"); return content.ToString(); }
So now we know exactly how our web service should behave. Let's begin with our cache for storing messages.
using System; using System.Collections.Generic; internal class AifMessageCache { private static List< string > messages = new List< string >(); public static void Clear() { messages = new List< string >(); } public static List< string > Messages { get { return messages; } } public static void Add(string message) { messages.Add(message); } }
Let's code our web methods ClearMessages, GetMessages, SendMessage and readPurchaseRequisition. It will be extremely simple because all we need to do is to delegate the call to our message cache.
[WebMethod] public void ClearMessages() { AifMessageCache.Clear(); } [WebMethod] public void SendMessage(string message) { AifMessageCache.Add(message); } [WebMethod] public void readPurchaseRequisition(string message) { AifMessageCache.Add(message); } [WebMethod] public List< string > GetMessages() { return AifMessageCache.Messages; }
For this code to compile we have to add support for generic collections:
using System.Collections.Generic;
We get one failure in TestSendMessage. This failure is caused by a wrong assumption. Our tests assume that the cache is empty when the test begins. That's not true and thus we have to call ClearMessages before every test. To do that update SetUp method in your test class:
[SetUp] public void SetUp() { service = new MyWebService.MyWebService(); service.ClearMessages(); }
Rebuild the solution and rerun the tests again. You should see all the tests passing.
Note:
static void RunAifServices(Args _args) { AifGatewaySendService gtwySend = new AifGatewaySendService(); AifOutboundProcessingService outSvc = new AifOutboundProcessingService(); ; print 'Running Outbound Processing Service...'; outSvc.run(); print 'Running Gateway Send Service...'; gtwySend.run(); print "AIF Services complete"; pause; }
What if our web service doesn't have readPurchaseRequisition but only SendMessage. Can we still configure AIF so that it calls this method instead of readPurchaseRequisition? Yes we can. Go to Endpoint configuration form, select your endpoint and go to endpoint action policies form by clicking Action policies. Navigate to readPurchaseRequisition row and enter SendMessage to External Identifier Override. This will force AIF to call SendMessage whenever there is a message for readPurchaseRequisition action.
Rerun the test and verify that the message was sent to the web service.
Problem: In case of a problem with Dynamics AX 4.0 you don't receive a valid error message but instead an error message saying:
Could not find any resources appropriate for the specified culture or the neutral culture.
Solution: This is a known error that was already fixed. The fix will be part of 4.01 release. If you want you can download updated DLL from here.
Now whenever there is an error with Outbound web service adapter in AIF you should receive a better error message containing the real cause of an error.
Problem: You keep on getting a SOAP error that it's not possible to call your web method. You are sure that everything is fine but you keep on getting this error message.
Solution: Try to verify that the namespace defined in your web service corresponds to the web service URL. If your web service URL is http://localhost/AifWebServiceTest/MyWebService.asmx then your web service namespace should be set that way:
Also, please note the trailing forward slash character. This all is very important!
You can download the complete solution at: AifOutboundWebService