Wednesday, October 19, 2011

Consuming External Webservices in AX 2012

For those of you who have read my post on the Windows Azure App, may recall my shortcut for fixing the missing bindings and other app.config settings. I've been meaning to dig into this further and come to a solution, but for the Azure post being constrained by my promised "10-minute app" I stuck with the ugly solution. Recently I was talking with the MSDN team about AIF related articles they are wanting to do, and I brought up this issue. They pointed out they had not seen this issue and asked if I had followed the whitepaper on consuming webservices. Now, there's not necessarily a lot to it, but in looking over the whitepaper I found one tiny little thing, which makes a world of difference and solves my issue: AIFUtil class! This sparked the idea of doing a follow-up on this, and due to a late night conversation on Twitter related to this I figured I really need to get this on my blog. The issue as I'm about to explain is a .NET fact and not an AX 2012 issue per se. In fact, as you'll see, AX 2012 has a way to fix the issue.

For this code walkthrough, I will use a free online web service for a currency converter. You can find the WSDL at http://www.restfulwebservices.net/wcf/CurrencyService.svc?wsdl. In case you this link is down by the time you read this, or if you just want to try something else, check XMethods for other free available web services online.

To get started, we'll create a new Visual Studio 2010 Class Library project, which I will name DAXMusings.Proxies


Next, we add a service reference. Right-click on the References node in your solution and select "Add Service Reference". In the address field, type our service URL http://www.restfulwebservices.net/wcf/CurrencyService.svc?wsdl and click "Go". In the Namespace field, type "CurrencyService". Click OK to generate the proxies.


Besides the proxy classes being generated by Visual Studio, it also puts all the web service bindings and information in the app.config file of your project. You can open it by double clicking on the app.config in the Solution Explorer.


Now, when an application loads a config file, it looks for the application's executable name and .config at the end. So on the AX client the Ax32.exe.config gets loaded. On the server side, Ax32Serv.exe.config file. Of course, our code is in the app.config, which is not helpful, it will never be loaded.
Let's see what happens. On the project, right-click and select "Add DAXMusings.Proxies to AOT".


Next, in the properties of the project, set the "Deploy to Client" property to "Yes".


Save the project and click Build / Deploy Solution. This will build and deploy your solution to the AX client.


Next, let's open the AX client. If you still had the client open, close it first and re-open. To do a quick and dirty test on the client, let's create a new job. If not open yet, open a developer workspace using CTRL+SHIFT+W. In the AOT, right-click and select New > Job.


In the code, we'll just create an instance of the service client, and call the service:


static void Job7(Args _args)
{
    DAXMusings.Proxies.CurrencyService.CurrencyServiceClient  service;
    DAXMusings.Proxies.CurrencyService.Currency currency;
    System.Exception ex;

    try
    {
        service = new DAXMusings.Proxies.CurrencyService.CurrencyServiceClient();
        currency = service.GetConversionRate(
            DAXMusings.Proxies.CurrencyService.CurrencyCode::USD,
            DAXMusings.Proxies.CurrencyService.CurrencyCode::EUR);
    
        info(strFmt('%1', CLRInterop::getAnyTypeForObject(currency.get_Rate())));
    }
    catch(Exception::CLRError)
    {
        ex = CLRInterop::getLastException();   
        info(CLRInterop::getAnyTypeForObject(ex.ToString()));
    }
}

Now, if you try to run this service, you get the error "Object 'CLRObject' could not be created". Not very helpful, and trying to catch a CLR Exception won't work either. If you look in the Windows Event Viewer, all you'll find is a warning that Dynamics AX is unable to load your assembly's config file. I'm unsure how to actually get the exception details in AX, so if anyone knows let me know. What I've done to get this, is basically create a static method in Visual Studio that I can debug. The error message I got out of that is:
Could not find default endpoint element that references contract 'CurrencyService.ICurrencyService' in the ServiceModel client configuration section. This might be because no configuration file was found for your application, or because no endpoint element matching this contract could be found in the client element.
So yes, the is the actual issue at play. The endpoint is in the app.config (in the output it becomes DAXMusings.Proxies.dll.config, check your user folder under AppData\Local\Microsoft\Dynamics AX\VSAssemblies where assemblies are deployed... check my blog post on Assembly deployment) and not in the config file of the executing host for our class library (AX32.exe.config).
And that is exactly what AIFUtil fixes! Change the code to the following:


static void Job7(Args _args)
{
    DAXMusings.Proxies.CurrencyService.CurrencyServiceClient  service;
    DAXMusings.Proxies.CurrencyService.Currency currency;
    System.Exception ex;
    System.Type type;

    try
    {
        type = CLRInterop::getType('DAXMusings.Proxies.CurrencyService.CurrencyServiceClient');
        service = AifUtil::createServiceClient(type);
        //service = new DAXMusings.Proxies.CurrencyService.CurrencyServiceClient();
        currency = service.GetConversionRate(
            DAXMusings.Proxies.CurrencyService.CurrencyCode::USD,
            DAXMusings.Proxies.CurrencyService.CurrencyCode::EUR);

        info(strFmt('%1', CLRInterop::getAnyTypeForObject(currency.get_Rate())));
    }
    catch(Exception::CLRError)
    {
        ex = CLRInterop::getLastException();
        info(CLRInterop::getAnyTypeForObject(ex.ToString()));
    }
}

And yes, that one works! If you look into the createServiceClient() method, you'll notice it actually loads the class library's config file. Nice! Problem solved!!

So, as a final note, on Twitter the question was asked, how do I differentiate between development and production? First I didn't get the question, but I get it now. If you are calling a custom service you've made, you may have a version of the service for development, and a separate version for production. Of course, the class library points to one and only one URL. So how do we make it point to different places in different environments without changing the code between the environments?

Change the config file? This would work, but the class library's config file is stored in the model store and downloaded by the client/server. It can't be changed unless it's changed in the AOT, the Visual Studio project is rebuilt, at which point the client/server will download the new version from the model store. So, you could copy/paste all the app.config settings into the AX32(serv).exe.config file and change it there. Then you won't need to use the aifUtil::createserviceclient. In any case, this is very impractical, especially for services running on the client side!

We can just go the AX route, and store the URL in a parameter table somewhere. Then, at runtime, we change the end point address with the following code (replace the hardcoded the localhost url with a parameter).


static void Job7(Args _args)
{
    DAXMusings.Proxies.CurrencyService.CurrencyServiceClient  service;
    DAXMusings.Proxies.CurrencyService.Currency currency;
    System.ServiceModel.Description.ServiceEndpoint endPoint;
    System.Exception ex;
    System.Type type;

    try
    {
        type = CLRInterop::getType('DAXMusings.Proxies.CurrencyService.CurrencyServiceClient');
        service = AifUtil::createServiceClient(type);
        //service = new DAXMusings.Proxies.CurrencyService.CurrencyServiceClient();
        endPoint = service.get_Endpoint();
        endPoint.set_Address(new System.ServiceModel.EndpointAddress("http://localhost/HelloWorld"));
        
        currency = service.GetConversionRate(
            DAXMusings.Proxies.CurrencyService.CurrencyCode::USD,
            DAXMusings.Proxies.CurrencyService.CurrencyCode::EUR);

        info(strFmt('%1', CLRInterop::getAnyTypeForObject(currency.get_Rate())));
    }
    catch(Exception::CLRError)
    {
        ex = CLRInterop::getLastException();
        info(CLRInterop::getAnyTypeForObject(ex.ToString()));
    }
}


That's all I got. Have fun with your SOA architecture! And as usual, this walkthrough was added to the other ones on the AX 2012 Developer Resources page!

13 comments:

  1. Hi joris,

    Nice post! It helped me configure the thing I needed to do but I still have a question.

    We have built a solution where we have several non-ax components that come into play. And we have opted to created data contracts in a .net assembly and all the components in the architecture use these datacontracts. So for example a CreateSalesOrder contract is the same for Ax as it is for Biztalk.

    That said, I want to create a service that uses the .NET datacontract as a parameter. This succeeded using the above method, but I was wondering if you need to add that assembly to the AOT or also create a proxy poject for it. And also, in that case, can normal dll references be used for that because then I get errors when generating service artifacts that the assembly is not referenced (although in the GAC etc...)

    Do I make sense here? :-)

    Kind regards,
    Kenny Saelen

    ReplyDelete
  2. that is really amazing,i was always save that in to deploy to Dp.and save that as yes.thanks so much for describing the whole procedure so clearly.
    Architecture Services
    Project Management Services

    ReplyDelete
  3. There is small problem with code like this. AifUtil::createServiceClient() will not work on AOS. Problem is related to xApplication::getVSAssembliesPath(), which is returning wrong path when running on server side. To avoid this, you will need to change AifUtil::createServiceClient method, and check:
    if (isRunninOnServer())
    vsAssembliesPath = System.IO.Path::Combine(xInfo::directory(Directory::Bin),"VSAssemblies");
    else
    vsAssembliesPath = xApplication::getVSAssembliesPath();

    ReplyDelete
  4. Hi

    I have been trying to get code like this running batch/AOS. If I'm debugging my problem correct, my code breaks at CLRInterop::getType() when trying to run under AOS.

    When you have found the right assembly path, how/where do you use it?

    Best regards
    Lars Vistrup Skov

    ReplyDelete
    Replies
    1. Hi Lars,

      have you made sure your VS project is set to deploy to server?

      Delete
  5. This is how you would catch the error in DAX:

    catch (Exception::CLRError)
    {
    ex = ClrInterop::getLastException();
    if (ex != null)
    {
    info( ex.get_Message() );
    info( ex.get_Source() );
    info( ex.get_StackTrace() );
    ex = ex.get_InnerException();
    if (ex != null)
    {
    error(ex.ToString());
    }
    }
    return false;
    }
    catch (exception::Internal)
    {
    ex = clrinterop::getLastException();
    if (ex)
    {
    info(ex.ToString());
    }
    return false;
    }
    catch (Exception::CodeAccessSecurity)
    {
    info("Code Access Security Error");
    return false;
    }


    Regards,

    Dwain

    ReplyDelete
  6. Hi Great Post,.
    my question is around parametrizing the url.
    in a situation where you may have a different url for prod and pre-prod, I havent been able to find a way to add the url to an AX table, make it a parameter and just switch it on the fly. instead, I am stuck either modifying the VS project and rebuilding it production, or creating 2 service references inside the project and use a condition to call the right one. none of these ideas are any good in my opinion, but I am open for suggestions :)

    ReplyDelete
    Replies
    1. Unless I misunderstand your question, the end of the article (including the code snippet listed) does exactly that...

      Delete
    2. you know, I should learn how to read, thanks

      Delete
  7. Mange mange TAK:) you have just saved my day!

    ReplyDelete
  8. Hi its there any way to add reference by code , suppose we have 5-6 url which will have same method and parameter and url would be pass as parameter to C# , is there any way to add web reference dynamically ?

    ReplyDelete
    Replies
    1. You don't add references dynamically. But just changing the URL is shown at the very end of this article.

      Delete