Using SOAP via a .NET in Dynamics AX

Posted
Comments 12

Our company was the first to use the European Commission's VIES system via SOAP to validate VAT numbers automatically. We used to have a custom hack to perform this operation within Axapta 3.0, waiting for what was then a rumour that Dynamics Ax 4.0 would be able to call .NET assemblies via CLR interoperability.

In my previous article, I touched on calling code within Ax from C#, but now I want to explain how this works the other way around.

To create a proper SOAP connector for VIES, I've used the wsdl.exe tool from the .NET Framework 2.0 SDK to generate the basis for a .NET Assembly from a WSDL definition (I'll use VIES's WSDL):

"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\wsdl.exe"
       /language:CS /protocol:SOAP
       http://ec.europa.eu/taxation_customs/vies/api/checkVatPort?wsdl

This creates a generated piece of code that works well, but needs some modification. You'll want to wrap the generated class in your own namespace, based on the name of your assembly. In my example, we also need to add a wrapper method, since Ax cannot properly handle variables in C# that use the out keyword. Without this, Ax will claim the method you're trying to call does not exist. You'll need this for any SOAP call that returns multiple variables. To return the variables, I've used an Array since it's an easy object to "unwrap" within Ax using standard System.Array methods. For my VIES example, I added this to the class:

[geshi lang=csharp]public Array check(string country, string vatNum) { bool valid; string name; string address; DateTime date; date = this.checkVat(ref country, ref vatNum, out valid, out name, out address); return new object[] {country, vatNum, valid, name, address, date}; }[/geshi]

Dynamics Ax will only call code within strong-named assemblies within the GAC, so you will need to generate a key and use it when you compile your code. From the SDK, you can use the sn.exe tool, or just use Visual Studio itself to set the parameters on the assembly correctly. If you're a command-line freak like me, use this:

"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\sn.exe"
       -k viessoap.snk
"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\csc.exe
       /target:library /keyfile:viessoap.snk
       /out:My.Namespace.Goes.Here.ViesSoap.dll
       .\checkVatService.cs"

To install the assembly in the GAC, you can drag-and-drop the DLL file using Windows Explorer into C:\WINDOWS\Assembly, or alternatively use gacutil.exe.

With that code compiled and installed, it's time to jump into Dynamics Ax 4.0, and create a reference to the assembly. Open up the AOT node for References and add your assembly. If it's correctly installed in the GAC and you opened Ax after you installed it, it should appear automatically in the grid. If not, go back and fix the installation. Make sure, once it's referenced, that the public key and version matches the assembly correctly.

Some versions of Ax have problems with this step, and you may find that your reference is not actually saved correctly. If this happens, a work-around is to create the reference, export it as an XPO file, and import it again immediately. You'll be able to tell if the reference will save correctly if you're viewing the AOT with layers shown: If the reference is not assigned a layer when you save it (i.e. CUS), then you'll need to perform the work around.

If the reference is installed well, we can now write a job to demonstrate this .NET call. You'll notice similarities with C# bleeding into X++ in the following example:

[geshi lang=xpp]static void checkViesJob(Args args) { // Permissions to use CLR interoperability methods InteropPermission interopPermission = new InteropPermission(InteropKind::ClrInterop); // Our class from our .NET assembly My.Namespace.Goes.Here.ViesSoap.checkVatService checkVatService; System.Exception clrException; System.Array resultsArray; AddressCountryRegionId countryRegionId = "BE"; VATNum vatNum = "0489123456"; boolean validity; str entityName; str entityAddress; date requestDate; ; // Fix the country code if we need to.. if (countryRegionId == "GR") { countryRegionId = "EL"; // Greek VAT numbers have a different prefix } // Fix up the VAT number, ready for processing use vatNum = TaxVATNumTable::stripVATNum(vatNum, vatNumPrefix.Prefix); // Request access to use the interoperability thingy interopPermission.assert(); try { // Create a new instance of the VAT check class checkVatService = new My.Namespace.Goes.Here.ViesSoap.checkVatService(); // Call the service (might take a few moments) startLengthyOperation(); resultsArray = checkVatService.check(countryRegionId, vatNum); endLengthyOperation(); // Pull out the results.. validity = ClrInterop::getAnyTypeForObject(resultsArray.GetValue(2)); entityName = ClrInterop::getAnyTypeForObject(resultsArray.GetValue(3)); entityAddress = ClrInterop::getAnyTypeForObject(resultsArray.GetValue(4)); requestDate = ClrInterop::getAnyTypeForObject(resultsArray.GetValue(5)); } catch (Exception::CLRError) { // Grab the last CLR exception object clrException = ClrInterop::getLastException(); // Lazily walk the CLR exceptions to try to be verbose.. while (clrException) { // Announce the error error(clrException.get_Message()); // Go to the inner exception clrException = clrException.get_InnerException(); } // Thow an Axapta error exception throw Exception::Error; } // We're done with our rights now, revert our permission request CodeAccessPermission::revertAssert(); // Show what we got from VIES info("Country: " + countryRegionId); info("VAT Num: " + vatNum); info("Validity: " + enum2str(validity)); info("Entity name: " + entityName); info("Entity address: " + entityAddress); info("Request date: " + date2str(requestDate, -1, -1, -1, -1, -1, -1)); }[/geshi]

Obviously, for VIES, I had to add additional code within Ax to fix country codes. For Greek VAT codes, the prefix is EL and not GR as you might presume for the other European countries. Also, VAT numbers in Ax can be entered with formatting (spaces, hyphens, and so forth) which need to be stripped out in the same manner as when they are sent electronically to tax authorities.

Deeper in, though, you'll notice the first thing we do is request permission. Ax has code security which protects code from unwittingly calling external services in situations it shouldn't be allowed to. You must assert your rights to call CLR objects before you can instantiate classes! As normal, make sure you clean up after yourself when you're done - This is where we revert our "assert".

You'll also notice that we can deal with native objects from .NET, such as System.Array easily, but converting back to native Ax types should be done via Ax's AnyType primitive type. In order to do this with some amount of intelligence, you should use ClrInterop::getAnyTypeForObject() to convert from a CLR object to Ax, and ClrInterop::getObjectForAnyType() to convert from Ax to a CLR object. Some objects will, however, automatically convert themselves, such as strings and integers, but be careful.

Ax will natively tell you some details about the class you're using via IntelliSense™, such as the methods you can call, however it is presently incapable of listing the variables you need to pass to those methods, so make sure you have the documentation or source code on hand.

If you're keen to find the C# code without the adventure, you can find it here. Of note is the check() call (the only modification), and as Dynamics AX cannot handle exceptions correctly, the catch statement is left empty and instead if an exception is thrown it simply returns an empty array.

Happy coding!


Categories X++, C#

Comments

  1. Hello, That is what i wanted to do, but i think i have some problem with changing the C# code ... Is it possible that you send me your C# script ? or you post it here ? Sorry for my poor english , i'm french .... Thanks. And have a good christmas ....
  2. (Author)

    Bonjour Dominique, Le Français n'est acun problème, mais mon Français est terrible! Désolé pour mon mauvais Français... J'ai téléchargé mon C# code et vous pouvez trouver à . J'ai également ajouté le URL à l'article. La seule modification est la méthode "check()". Dynamics AX ne peut pas accepter des exceptions de .NET correctement, donc "catch" ne fait rien et s'il y a une erreur, une rangée vide est retournée. In case my French is too terrible, here it is in English: My code is uploaded at the above URL. The only modification is the "check()" call. Dynamics AX cannot properly handle .NET exceptions, so the "catch" statement is empty so if there's an error it simply returns an empty array. Merci, et un joyeux noël à vous aussi! - Simon
  3. Thanks a lot , it works !!!
  4. et votre français et parfait !
  5. Hello, I have another question about the vatnum. Do you know why in Axapta the vatnum of a customer is store in a table and not in a field ? Because a user could change it ... I haven't found any answer
  6. (Author)

    Hi Dominique, The VAT number is linked to other things such as sales orders and purchase orders (and ultimately invoices) and can have significant tax authority consequences if an invoice was produced and then the VAT number is changed. For future reporting of past invoice events to tax authorities, you would want to know the legal name and originating country for that VAT number at the time of the invoice, rather than the present name/country of a customer/vendor record. DAX does similar things in the system; for example, have a look at the tables related to customer or vendor invoices and you'll see how important these relationships are for back-dated reporting. This is the main reason for this that I can see, so I hope this makes sense! As far as user security goes, there's no reason why you can't remove access to the VAT table and replace the VAT number field with an _edit method_ that does some magic to conceal it from the user. Cheers, - Simon
  7. Thanks for the answer. But I still don't understand , because a The "TVA intracommunautaire" is unique for the customer or the supplyer ... so it shouldn't have any change on it .... Thanks again. Dominique
  8. (Author)

    Hi Dominique, Another reason may simply be _database normalisation_. The VAT numbers are used in many, many tables, and a heavy dose of normalisation is common in OLTP database design. Normalising this makes sense, for example when maintaining a list of valid VAT numbers the relation back to _TaxVATNumTable_ from other tables is always verified by the user interface. In our case, we also store the VIES validity we acquired against the VAT number records within _TaxVATNumTable_, giving it a clearer purpose. I suppose you could consider _TaxVATNumTable_ to be quite similar to the _AddressZipCode_ table in some respects. Cheers, - Simon
  9. regarding this concept/technique, i created a custom dll, strong key'd, installed on AOS. All the clients are now getting a "failed loading assembly...." Does the DLL need to be deployed to all clients in order to utilize this technique of using a .net dll?
  10. (Author)

    Hi Gary, When using a CLR (.Net) assembly, you either must install it within the GAC on each AOS or within the GAC of all clients that will use the code. The decision is up to you, depending on what your circumstances, and depends only on the _tier_ your code is executing within. For something like this simple VAT/Tax number validation routine, it makes no sense to have the burden of rolling out the assembly to all clients, so the assembly resides only on the AOS servers. The difference within the X++ side of the code is that the class is configured to explicitly execute on the server. The situation for static methods is slightly different though, because you will want to add the "server" keyword to the method's definition too, otherwise it may still execute on the same tier it was called from. If the clients are still receiving the message, use the debugger to make sure the code calling or even referencing the assembly is actually being executed on the server tier instead of the client tier. Normally you can see this quickly by way of the little icons next to the methods in the stack trace pane. Good luck! - Simon
  11. Hi, Is it actually possible to create a client in Axapta for a WebService which has some ComplexTypes in the Request / Response ? I'm trying to find more information on that, but didn't have any luck so far :-( Regards, Stefaan
  12. (Author)

    Hi Stefaan, Sorry for taking so long to get back to you. Good question, and I'm sorry to say that I haven't really had that much experience with ComplexTypes defined in a web service for _Dynamics AX_. However, I would imagine that the procedure would be mostly the same - if you generate code based on the WSDL you should then be able to use it as an assembly in _Dynamics AX_. The trick would be mapping data-types into those _Dynamics AX_ can use. I'm unsure about _Dynamics AX 2009_ (version 5) as we haven't started the a migration project yet, but _Dynamics AX 4.0_ has very immature CLR interoperability. In particular, the use some data types through a CLR API cause a high number of page faults (leading me to think that there's a lot of memory copying going on), and some data types cannot be supported at all through CLR. To this end, I would suggest extending the generated C# code to include wrappers that simplify access to the data returned from the web service. A a simplistic example, _Dynamics AX 4.0_ does not handle any of the generics very well. For example, a generic list object will need to be converted into an array before _Dynamics AX_ can use it. Ultimately, if this solution doesn't work then you can try reversing the concept; Don't forget that _Dynamics AX 4.0_ comes with a few _Business Connector_ licenses as standard, and you can use the ".Net Connector" to write your own code to handle the web service, which uses an easy API within X++ to push the data into _Dynamics AX_. This of course depends on how your web service needs to work, though (_push_ vs. _pull_ paradigms). If you do find a solution, please let me know. Good luck! Cheers, - Simon

Commenting has expired for this article.