Web Services Example 7 – Call NAV OData Web Services (Part 1)

24 Oct

The Web Services series was started almost a year ago, after NAV Techdays 2015. All demos that I demonstrated have now been published. But I promised an extra example: call NAV OData web services directly from C/AL code. And at Directions EMEA 2016, a few weeks ago, I demonstrated that it is possible to call NAV OData web services from C/AL, without any custom dll, using .Net Framework library. So this post is first meant to deliver on the promise and secondly, to post the example that was showed at Directions EMEA. In other words, I try to kill two birds with one stone. Smile

As you can see in the title, this is part 1. Originally I started to write a very looong post. But because I couldn’t stop writing, I decided to split it in two parts. First part is about reading from NAV OData web services. Second part will demonstrate how to write to NAV OData web services.

The code is available for download from Github:
https://github.com/ajkauffmann/DynamicsNAV

No assemblies

In this example we will reuse the basic pattern as explained in this post. This allows us to call any OData web service, so why not using it for NAV web services? The code runs directly from C/AL and we only use the .Net Framework library. No Visual Studio, no custom assemblies, just C/AL code and .Net Interoperability. Sounds promising, right?

Before we look at the code, I have to explain a code change to the basic pattern. Instead of passing a bunch of parameters, I changed the code to pass in a .Net dictionary that contains all relevant parameters. As a result, the code is more loosly coupled and we can add parameters if we need it, without having to refactor existing code that uses the same function but don’t need that parameter. All inspired by this blog post of Vjeko. I didn’t take over the complete pattern of using a TempBLOB. But that’s for a reason. The response object that is returned cannot easily be serialized. If I did, the code would become too complex. So for this reason I decided to keep the HttpResponseMessage parameter but wrap the input parameters into a .Net Dictionary.

With NAV web services you need to specify valid credentials together with the call. According to the documentation, there are several options. In this example I use the web access key as described in this MSDN article.

A lot of existing code examples show how to use Visual Studio to create a service reference and how to set credentials on this. But we are using HttpClient, a generic object in the .Net Framework Class Library to call whatever web service. How can we provide credentials with HttpClient?

The answer is: use HTTP request headers.

Let’s quickly go over some theory here. HTTP messages have a header and a body. The header contains metadata, such as the HTTP method (GET / POST /  PUT / DELETE), encoding information and information about the type of data that is accepted. Browsers also add information to the header over the browser name and version that is sending the request. The body is often just empty. If the body contains data that you want to transmit, then the header contains information and instructions how to use that data. On this website you can find more information about the basics of HTTP.

Ok, got it, we will provide our NAV credentials in the HTTP request header. How does that work with HttpClient?

Well, that’s simple: we set HttpClient.DefaultRequestHeaders.Authorization to the correct value. Let’s look at the code:

[TryFunction] CallRESTWebService(VAR Parameters : DotNet "System.Collections.Generic.Dictionary`2";VAR HttpResponseMessage : DotNet "System.Net.Http.HttpResponseMessage")
HttpClient := HttpClient.HttpClient();
HttpClient.BaseAddress := Uri.Uri(FORMAT(Parameters.Item('baseurl')));

IF Parameters.ContainsKey('accept') THEN
  HttpClient.DefaultRequestHeaders.Add('Accept',FORMAT(Parameters.Item('accept')));

IF Parameters.ContainsKey('username') THEN BEGIN
  bytes := Encoding.ASCII.GetBytes(STRSUBSTNO('%1:%2',FORMAT(Parameters.Item('username')),FORMAT(Parameters.Item('password'))));
  AuthHeaderValue := AuthHeaderValue.AuthenticationHeaderValue('Basic',Convert.ToBase64String(bytes));
  HttpClient.DefaultRequestHeaders.Authorization := AuthHeaderValue;
END;

IF Parameters.ContainsKey('httpcontent') THEN
  HttpContent := Parameters.Item('httpcontent');

CASE FORMAT(Parameters.Item('restmethod')) OF
  'GET':    HttpResponseMessage := HttpClient.GetAsync(FORMAT(Parameters.Item('path'))).Result;
  'POST':   HttpResponseMessage := HttpClient.PostAsync(FORMAT(Parameters.Item('path')),HttpContent).Result;
  'PUT':    HttpResponseMessage := HttpClient.PutAsync(FORMAT(Parameters.Item('path')),HttpContent).Result;
  'DELETE': HttpResponseMessage := HttpClient.DeleteAsync(FORMAT(Parameters.Item('path'))).Result;
END;

HttpResponseMessage.EnsureSuccessStatusCode(); // Throws an error when no success

On lines 8 to 12 the authentication header is created and saved into the request headers. As you can see, it is basically string in the format <username>:<password> that is then encoded as a Base64 string. It is strongly recommended to use a SSL certificate to encrypt to data, otherwise your username and password are at risk to be exposed to others.

A difference with the previous web service examples is that NAV OData web services can return data in different formats: XML format (AtomPub document) and JSON format. By default the XML format is used. To get the response in JSON, we need to add $format=json to the url. Well… not really… I’ll show you an alternative.

Look at line 6 in the code example above. The HTTP header ‘Accept’ tells the server what type of data we expect. If we specify that we expect JSON data, then we don’t have to add the $format=json parameter to the url. The correct Accept header value for this is: ‘application/json’. In the example code I use this header value, instead of the url parameter $format=json.

In the web service example  about verifying an e-mail address, I demonstrated how to create a .Net object that reads the returned JSON data. However, in this example I want to introduce a different way of reading JSON data. Using the Newtonsoft.Json.dll that is already available in every Dynamics NAV installation. The only thing you need to do is to copy that dll from the Service folder to the Add-in folder. Only on your development machine! It will automatically be loaded from the Service folder at runtime.

How can we read data using the JSON assembly? That’s done by using the JToken, JArray and JObject classes. In the download on Github I have included a helper Codeunit that converts JToken objects to real C/AL values. I will explain further when we look at the example code.

Reading NAV OData Web Services

Let’s first start with reading from a NAV OData web service. That’s more or less the same as with other web service examples that I have posted earlier.

In the example Codeunit I have provided four examples:

  • Example of converting different data types
  • Reading the full item list
  • Reading a filtered list of sales orders
  • Reading the lines of a specific sales order

Besides those example, I have some helper functions. Those for reading username and password should of course be modified into more decent code, using setup tables. For sake of simplicity, I have just decided to use fixed values here. And there is a function to compose the URL path for the OData web service, according to the NAV rules. I assume that this function has no secrets, it’s fairly straight forward.

The example of converting different data types consist needs Page 72000 Test Json Values to be published as a web service with the name ‘TestJsonValue’. Let’s now look at that first function, it already contains 80% of all code that is needed to call a NAV OData web service.

LOCAL TestJsonValues() 
Parameters := Parameters.Dictionary(); 
Parameters.Add('baseurl',BaseUrl); 
Parameters.Add('path',GetODataPath('TestJsonValues')); 
Parameters.Add('restmethod','GET'); 
Parameters.Add('accept','application/json'); 
Parameters.Add('username',GetUserName); 
Parameters.Add('password',GetPassword); 

RESTWSManagement.CallRESTWebService(Parameters,HttpResponseMessage); 

result := HttpResponseMessage.Content.ReadAsStringAsync.Result; 

JToken := JToken.Parse(result); 
JArray := JToken.SelectToken('value'); 
JObject := JArray.First; 

MESSAGE('%1\\' + 
        'TextValue: %2\' + 
        'IntegerValue: %3\' + 
        'DecimalValue: %4\' + 
        'DateValue: %5\' + 
        'TimeValue: %6\' + 
        'DateTimeValue: %7\' + 
        'BooleanValue: %8', 
        result, 
        JsonHelperFunctions.GetValueAsText(JObject,'TextValue'), 
        JsonHelperFunctions.GetValueAsInteger(JObject,'IntegerValue'), 
        JsonHelperFunctions.GetValueAsDecimal(JObject,'DecimalValue'), 
        JsonHelperFunctions.GetValueAsDate(JObject,'DateValue'), 
        JsonHelperFunctions.GetValueAsTime(JObject,'TimeValue'), 
        JsonHelperFunctions.GetValueAsDateTime(JObject,'DateTimeValue'), 
        JsonHelperFunctions.GetValueAsBoolean(JObject,'BooleanValue')); 

As you can see, I have added to the ‘accept’ parameter with value ‘application/json’. Until the call of the web service, there is no further difference with previous examples, except for the usage of a .Net dictionary instead of separate parameters.

The difference kicks in with the line that reads the result into a JToken variable. Let’s have a look at the JSON text that is returned from the server:

image

The data in the tag value is surrounded by square brackets. This means that the data contains an array. In this case, the array only contains one object, which is surrounded by curly brackets.

Now take a look at the code that reads this data. First a JToken is created by parsing the complete JSON text. A JToken is a generic JSON object, it can be anything. From this JToken, the member ‘value’ is read, by calling JToken.SelectToken(‘value’ ). Since I know that this is an array, I pass this straight into an JArray.

Next step is to get the object out of the array. In this case, the array contains just one object. So JArray.First returns me that JObect. This JObject contains a number of properties, like TextValue, IntegerValue, etc. With JObject.GetValue I can read the value of a specific property.

There is on thing to be aware of: the JObject.GetValue returns a real object, a JToken. This JToken can be used to create a real .Net oject. With the ToString method of JToken you get the text out of it. And with ToObject you can create a corresponding .Net type.

In the Codeunit Json Helper Functions I have included a number of functions to convert the most common values into C/AL variables. Here is the code from this Codeunit:

GetValueAsText(JObject : DotNet "Newtonsoft.Json.Linq.JObject";PropertyName : Text) ReturnValue : Text
ReturnValue := JObject.GetValue(PropertyName).ToString;

GetValueAsInteger(JObject : DotNet "Newtonsoft.Json.Linq.JObject";PropertyName : Text) ReturnValue : Integer
ReturnValue := DotNetInteger.Parse(JObject.GetValue(PropertyName).ToString);

GetValueAsDecimal(JObject : DotNet "Newtonsoft.Json.Linq.JObject";PropertyName : Text) ReturnValue : Decimal
ReturnValue := DotNetDecimal.Parse(JObject.GetValue(PropertyName).ToString,CultureInfo.InvariantCulture);

GetValueAsDate(JObject : DotNet "Newtonsoft.Json.Linq.JObject";PropertyName : Text) ReturnValue : Date
DotNetDateTime := JObject.GetValue(PropertyName).ToObject(GETDOTNETTYPE(DotNetDateTime));
ReturnValue := DT2DATE(DotNetDateTime);

GetValueAsTime(JObject : DotNet "Newtonsoft.Json.Linq.JObject";PropertyName : Text) ReturnValue : Time
DotNetDateTime := JObject.GetValue(PropertyName).ToObject(GETDOTNETTYPE(DotNetDateTime));
ReturnValue := DT2TIME(DotNetDateTime);

GetValueAsDateTime(JObject : DotNet "Newtonsoft.Json.Linq.JObject";PropertyName : Text) ReturnValue : DateTime
DotNetDateTime := JObject.GetValue(PropertyName).ToObject(GETDOTNETTYPE(DotNetDateTime));
ReturnValue := DotNetDateTime;

GetValueAsBoolean(JObject : DotNet "Newtonsoft.Json.Linq.JObject";PropertyName : Text) ReturnValue : Boolean
ReturnValue := DotNetBoolean.Parse(JObject.GetValue(PropertyName).ToString);

The rest of the examples are basically the same as the first example. The only differences are to demonstrate some other possibilities.

Let’s quickly go over those differences. From the read of the item list, this code part is different:

JToken := JToken.Parse(result);
ItemList := JToken.SelectToken('value');

FOREACH Item IN ItemList DO BEGIN
  TempItem.INIT;
  TempItem."No." := Item.GetValue('No').ToString;
  TempItem.Description := Item.GetValue('Description').ToString;
  TempItem.INSERT;
END;

ItemList is a JArray, while Item is a JObject. The FOREACH makes sense, right? The JToken ‘value’ now contains more than one object.

The read of sales orders could be the same as with the item list. But I want to draw your attention tho this line:

Parameters.Add('path',GetODataPath('SalesOrder') + '?$filter=Sell_to_Customer_No eq ''10000''');

Here I have filtered the list of sales orders for just one customer. Hardcoded, for simplicity reasons. I’m sure you can change this to a better way of coding. Smile

Finally, we want to look at sales lines. The OData web service for sales orders is Page 42. This page contains the lines as a subpage. However, the resulting JSON value doesn’t contain those lines. Instead, the lines can be read as an extra resource. We call this a containment. For more info, see this page.

Lines can be read by extending the URL of the SalesOrder as you can see here:

Parameters.Add('path',GetODataPath('SalesOrder') + '(Document_Type=''Order'',No=''' + SalesOrderNo + ''')/SalesOrderSalesLines');

The result of this line looks like:

DynamicsNAV90_WS/OData/Company(‘CRONUS%20International%20Ltd.’)/SalesOrder(Document_Type=’Order’,No=’101016′)/SalesOrderSalesLines

Please note the bold part. This points to a specific record. Think of that record as a resource. Then on that resource, we ask for the SalesOrderSalesLines. You get the picture? By the way, the same happens with the Company part in the URL.

Well, that’s it for now. Next part will be about writing to NAV OData web services. Stay tuned!

Leave a Reply

Your email address will not be published.