Using SOAP via a .NET in Dynamics AX

Axapta

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:

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};
}

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:

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));
}

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!

Trackbacks

Comments

Display comments as Linear | Threaded

Dominique on :

Simon Butcher on :

Dominique on :

Dominique on :

Dominique on :

Simon Butcher on :

Dominique on :

Simon Butcher on :

Simon Butcher on :

Stefaan Lesage on :

Simon Butcher on :

The author does not allow comments to this entry

Add Comment

E-Mail addresses will not be displayed and will only be used for E-Mail notifications.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

Textile-formatting allowed