Lösungen
Märkte
Referenzen
Services
Unternehmen
Connecting to a NAV WebService from JavaScript

Connecting to a NAV WebService from JavaScript

19. Oktober 2016

Connecting to a NAV WebService from JavaScript

I needed to create a HTML / JavaScript frontend for a Dynamics NAV 2013R2 WebService and came across Freddy Kristiansen’s blog post from 2010. It was really helpful and got me started but fortunately especially in JavaScript there was a lot of progress since then and it got easier. I am mainly using the same scenario and will do the same WebService calls as in the original post, so it makes sense to read it before you get to the updated part below. I also think the exact same code should still work up to NAV 2017 but I didn’t test it thoroughly.

The following things are different in my setup:

To better understand the code and explanations, here are systems in play:

Implementation

In a slight variation of Freddy’s initial implementation, I created a simple HTML page with 3 links that call the respective WebServices. The main call can be seen in InvokeNavWS. It is quite a bit easier than before as the JSON/OData call is better supported in JavaScript. We are creating a GET request (line 12) to the WebService we want and tell it to return JSON (line 13). Lines 15 to 17 take care of authentication. As the request is done asynchronously we define a callback method in line 18. That method is called as soon as the WebService returns the result.

The following functions are used for executing InvokeNavWS for the three different WebService calls and then handling the results. If you e.g. click on the “get Companies” link, this calls SystemService_Companies because of line 84. This in turn calls the Company WebService in line 25 and sets HandleReturn_Companies as callback on success. Lines 30 to 34 than go through the results and put it on the page in line 35. The other two links work very similar.

The full code is

<html> 
    <head> 
        <title></title> 
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
        <script type="text/javascript"> 
            var baseURL = 'http://t00-dll-test/cronus_ws/'; 
 
            // Function to Invoke a NAV WebService 
            function InvokeNavWS(method, parameters, successFunc) { 
                $.ajax({
                    type: "GET",
                    url: baseURL + method + '?$format=json' + parameters,
                    contentType: 'application/json; charset=utf-8',
                    xhrFields: {
                        withCredentials: true
                    },
                    success: successFunc,
                    error: HandleError
                }); 
            } 
 
            // Get the Company list 
            function SystemService_Companies() { 
                InvokeNavWS('Company', '', HandleResult_Companies); 
            } 
 
            // Handle the results from the Company list
            function HandleResult_Companies(result) {
                var companies = result.value;
                var companyText = "";
                for (var i = 0; i < companies.length; i++) { 
                    companyText += companies[i].Name + '<br>'; 
                } 
                $('#companies').html(companyText);
            }
 
            // Get details for a specific Customer
            function CustomerPage_Read(no) { 
                InvokeNavWS('CustomerList', '&$filter=No eq \'' + no + '\'', HandleResult_Customer); 
            } 
 
            // Handle the results from the Customer call
            function HandleResult_Customer(result) {
                $('#cust10000').html('Name of Customer 10000: ' + 
                        result.value[0].Name + '<br>'); 
            }
 
            // Get details for multiple Customer
            function CustomerPage_ReadMultiple(filters) { 
                InvokeNavWS('CustomerList', filters, HandleResult_MultipleCustomer); 
            } 
 
            // Handle the results from the Customer call
            function HandleResult_MultipleCustomer(result) {
                var Customers = result.value;
                var customersText = "Customers in ES served by ROT or BLAU warehouse:<br>";
                for (i = 0; i < Customers.length; i++) {
                    customersText += Customers[i].Name + ', Contact ' + Customers[i].Contact + '<br>';
                }
                $('#custList').html(customersText);
            }
 
            function HandleError(err) {
                console.log(err);
            }
 
        </script> 
    </head>  
    <body> 
        <div id="companies">
            <a href="#">get Companies</a>
        </div>
        <br>
        <div id="cust10000">
            <a href="#">get Customer 10000</a>
        </div>
        <br>
        <div id="custList">
            <a href="#">get Customers in ES served by ROT or BLAU warehouse</a>
        </div>
         
        <script type="text/javascript"> 
            $("#companies a").click(function () { SystemService_Companies(); }); 
            $("#cust10000 a").click(function () { CustomerPage_Read(10000); });
            $("#custList a").click(function () { CustomerPage_ReadMultiple("&$filter=Country_Region_Code eq 'ES' and (Location_Code eq 'ROT' or Location_Code eq 'BLAU')"); });
        </script> 
    </body> 
</html>

Things to note

Because of the JSON implementation we can address the Customer fields directly by name. E.g. in line 59 we are using the Customer Name and Contact fields without having to handle the complicated XML structure returned by SOAP WebServices.

Filters are also different from the SOAP calls as they are directly added to the URL as explained here and used in lines 40 or 86. This is very convenient to develop and test as you can just put the search request into your browser and see the results. The syntax is a bit unusual for a C/AL developer but quite powerful with features like indexof, substring or trim.

A note for real life

As you saw we didn’t specify any company information in our calls to the WebService. This will work if you only have one company or can define a default company in the NAV Server configuration. But probably that won’t be enough and you’ll have to add the company in your URLs to be able to switch between companies. This is easily done by adding Company(‘<company name>’) between the instance name and the WebService name in the URL. E.g. if you wanted to address Company “CRONUS AG” the URL would be <remote-proxy-url>/Company(“CRONUS%20AG”)/<web-service> or in my example http://t00-dll-test/cronus_ws/Company(“CRONUS%20AG”)/CustomerList. As you can see the Company name is URL encoded (blank is converted to %20). Be careful with the spelling as this is case sensitive. Now we are done with the functionality itself and can tackle the security issue we need to solve for the implementation to work.

What is the cross-domain access problem about?

The problem from a security standpoint is that JavaScript code might do problematic things if it were allowed to call different sites. To quote Wikipedia: “In computing, the same-origin policy is an important concept in the web application security model. Under the policy, a web browser permits scripts contained in a first web page to access data in a second web page, but only if both web pages have the same origin. An origin is defined as a combination of URI scheme, hostname, and port number. This policy prevents a malicious script on one page from obtaining access to sensitive data on another web page through that page’s Document Object Model.”

Therefore all modern browsers prevent scripts to access URLs from sites other than the one where they are hosted. In our example the HTML / JavaScript page is hosted on t00-dll-test, but the NAV WebService we want to call is running on s00-nst-dev. There a number of different solutions for that but probably the most popular are setting so-called CORS headers (which basically tell the browser that cross-domain access is ok for that special target) or using a reverse proxy.

Because setting CORS headers is not directly possible for the NAV WebService we need to put something between the caller and the WebService which either does CORS or the reverse proxy. As we already have our IIS serving the HTML / JavaScript page and implementing a reverse proxy is quite simple, I chose that way. The idea here is to allow the JavaScript code to call the IIS, therefore stay on the same site and prevent any security blocks the browser might do otherwise. The call gets routed by the IIS to the NAV Server and the response gets routed back.

On a side note this might also help with other security considerations as you don’t need to expose your NAV Server instance or at least the WebService port completely but can control what exactly is called through an IIS in a DMZ.

How to implement a reverse proxy?

First of all I suggest you read the very good blog post by Scott Forsyth about creating reverse proxies in general.

The step by step description of what I did are as follows: First you need to run the Web Platform Installer to get ther URL Rewrite module

 

After the installation finishes you should see the “URL Rewrite” module in the IIS Manager

You open it and click on “Add Rule(s)…” in the Actions bar on the right

Now you select the “Reverse Proxy” rule

If you haven’t installed Application Request Routing (ARR) then the following steps will guide you through the installation process which should only take a minute. If you create the first Reverse Proxy Rule on that IIS then you will be asked to confirm that request routing should be enabled. After that you will be able to configure the rule. In my example I want to redirect all incoming requests to <iis-server>/cronus_ws to my WebService, i.e. <nav-server>:<odata-port>/<instancename>/OData and I won’t use SSL. Therefore my rule is:

Now we need to configure that rule a bit more and select that rule and click on “Edit…” under “Inbound Rules” on the right

The pattern I want to use is cronus_ws, therefore I enter “cronus_ws/(.*)” in the Pattern input

Because I don’t want to expose the full instance (and make my URLs shorter), I change the
“Rewrite URL” in the “Action” tab to “{C:1}://s00-nst-dev:9148/Cronus_Extended/OData/{R:1}”.

This causes any request to t00-dll-test/cronus_ws to be redirected (or “remote proxied”) to that URL with everything after the pattern being appended. To give you an example a request to t00-dll-test/cronus_ws/Company will be redirected to s00-nst-dev:9148/Cronus_Extended/OData/Company and the result will be returned to the caller.

Now we click on “Apply” in the top right and our rule is working. To be honest, I was kind of surprised how easy it is and how well it worked in my experience. You can change the Rewrite URL to whatever makes sense for you. If you e.g. only want to expose the Customer WebService you might change the rule to <nav-server>:<odata-port>/<instancename>/OData/Customer/(.*) and no one is able to use anything but that WebService through your remote proxy. Keep in mind that this will maybe be a problem with company selection, see “A note for real life”