My Soap Service Proxy Codeunit

Up to now we in Advania have been using the method described here on my blog to connect to most of the Soap web services that we needed to integrate with.

The problem with this method is that we have to manage a lot of DLLs.  This has caused some issues and problems.

Another thing is that we are moving to AL.  And in AL we can’t just throw in a custom DLL to do all the work.

In C/AL We can do this with standard dotnet objects

AL code to do the same with the built in AL objects but that code is not much shorter.

With a custom proxy DLL the code would be

With this example we can easily see why we have chosen to create a proxy DLL for most of the Soap services.

I wanted to find a way to make things easier in AL and I remembered having dealt with C/AL objects by Vjeko from some time ago.  I took another look and that code helped me to get started.

The result is a Soap Proxy Client Mgt. Codeunit in C/AL that I have sent to Microsoft’s cal-open-library project asking to have this code put into the standard C/AL library.

Using this Codeunit the code will be like this.

What about AL?

For now this C/AL Codeunit is not in the standard CRONUS database.  I need to import the C/AL code and make sure that AL will be able to use that Codeunit.  You can see how to do this in my last blog post.

This C/AL Code will directly convert to AL and is ready to use.

More examples on how to use this Proxy Codeunit will follow.  Stay tuned…

Windows 10 and Universal Apps

A new operating system from Microsoft sure has it’s impact on our ecosystem.  Our Microsoft Dynamics NAV is based on top of Microsoft operating systems.

With the power of SOAP and OData services in Microsoft Dynamics NAV everyone is now able to design a custom user interface dedicated for a simple task, both for employees and customers.

It should be interesting to grab the Visual Studio 2015 tools and start to experiment with the Windows Universal developement with Microsoft Dynamics NAV on the back-end.

The end goal, to have Windows running on all type of devices will open a lot of possibilities.  Go wild…

Using NetTcpPortSharing for NAV Servers

I just came back from three weeks vacation yesterday.  During my vacation I had made a decision to implement Tcp Port Sharing for the Instance Administration tool used in Advania Azure.

Early last year I published a function that uses the sc.exe to modify a NAV Service startup type.  When a NAV Service is installed and configured in setup, the startup type is Automatic (Delayed Start).  However, create a new service with Powershell New-NavServerIntance and the statup type is Automatic without the (Delayed Start).

To enable Tcp Port Sharing that same sc.exe function is needed.  Interestingly, after I had finished the task and was reading NAV blogs I saw that Waldo just published a powershell function on his blog to do just this.

The script lines I used and added to my Instance Administration powershell scripts are based on my fist sc.exe function but not using the function it self.  Now when a new NAV service is created by the tool the startup type is modified and if so selected by the deployment settings, the Tcp Port Sharing is also activated.

By default, the Tcp Port Sharing service is disabled.
servicedisabled

The startup type should be changed to Manual.  This can be done manually or by an administrative powershell script.

[code lang=”powershell”]#Set Startup Mode for NetTcpPortSharing to Manual
$command = ‘sc.exe \\$Computer config "NetTcpPortSharing" start= demand’
$Output = Invoke-Expression -Command $Command -ErrorAction Stop
if($LASTEXITCODE -ne 0){
Write-Error "$Computer : Failed to set NetTcpPortSharing to manual start. More details: $Output"
}
[/code]

Similar script is used to update the existing NAV Services to both delayed start and Tcp Port Sharing dependency.

[code lang=”powershell”]

#Stop NAV Server Instances
Get-NAVServerInstance | Set-NAVServerInstance -Stop
#Update Startup Type and Dependency on NAV Server Instances
Get-NAVServerInstance | foreach {
$Service = $_.ServerInstance
Write-Host "Working on service $Service"
$Computer = ‘LOCALHOST’
$command = ‘sc.exe \\$Computer config "$Service" start= delayed-auto’
$Output = Invoke-Expression -Command $Command -ErrorAction Stop
if($LASTEXITCODE -ne 0){
Write-Error "$Computer : Failed to set $Service to delayed start. More details: $Output"
}
$command = ‘sc.exe \\$Computer config "$Service" depend= NetTcpPortSharing/HTTP’
$Output = Invoke-Expression -Command $Command -ErrorAction Stop
if($LASTEXITCODE -ne 0){
Write-Error "$Computer : Failed to set $Service TcpPortSharing. More details: $Output" -foregroundcolor red
}

}
#Start NAV Server Instances
Get-NAVServerInstance | Set-NAVServerInstance -Start
[/code]

It should be obvious that the above script can also use the Set-ServiceStartupMode from my blog and the Enable-NAVServerInstancePortSharing function on Waldo’s blog. That would be a cleaner code and more in line with what we would like to see.

Again quoting Waldo from his previous blog, “When you’re using a dedicated service account, things might become a slight more difficult”.  That is exactly my case, I am using a dedicated service account.

After enabling Tcp Port Sharing and updating the services they would not start.  Event Viewer revealed the reason.

Server instance: CRONUS
The service MicrosoftDynamicsNavServer$CRONUS failed to start. This could be caused by a configuration error. Detailed error information:System.ServiceModel.CommunicationException: The service endpoint failed to listen on the URI ‘net.tcp://mynavserver.dynamics.is:7046/CRONUS/Service’ because access was denied. Verify that the current user is granted access in the appropriate allowAccounts section of SMSvcHost.exe.config. —> System.ComponentModel.Win32Exception: Access is denied

So I started to ask Bing what I could do.  Microsoft MSDN states:

When a net.tcp binding enables port sharing (by setting portSharingEnabled =true on the transport binding element), it implicitly allows an external process (namely the SMSvcHost.exe, which hosts the Net.TCP Port Sharing Service) to manage the TCP socket on its behalf.

Hence, I need to add the Sid of my NAV Service Account to the SMSvcHost.exe.config file.  I could do this manually, but I am a programmer!

Another powershell script was born.  This one could also be converted to a function.  Before executing the script make sure to update the user and domain in the top of the script.  Be smart and execute this function before updating the NAV Services with the script above.

[code lang=”powershell”]
#Modify User and Domain to fit your environment
$UserToAdd = ‘srvNAV’
$UserDomainToAdd = ‘DYNAMICS’

#Initial Values
$UserSidFound = ‘false’
$ConfigurationSet = ‘false’

#Net.Tcp Port Sharing Service Name
$ServiceName = ‘NetTcpPortSharing’

#Get SID for the Service User
$UserSid = ([wmi] "win32_userAccount.Domain=’$UserDomainToAdd’,Name=’$UserToAdd’").SID

#Get Path for SMSvcHost.exe.config file
$SMSvcHostPath = (Get-WmiObject win32_service | ?{$_.Name -like $ServiceName} ).PathName
$SMSvcHostPathConfig = $SMSvcHostPath + ‘.config’

Write-Host "Reading XML from $SMSvcHostPathConfig"
#Read Config file
$xmlDoc = [xml] (Get-Content $SMSvcHostPathConfig)

Write-Host "Looking for access permission for $UserSid"
#Loop through allowed accounts and search for the service user Sid
$allowAccounts = Select-Xml "configuration/system.serviceModel.activation/net.tcp/allowAccounts/add" $xmlDoc
$allowAccounts | ForEach-Object {
$ConfiguredSid = $_.Node.Attributes.Item(0).Value
if ($ConfiguredSid -eq $UserSid) {$UserSidFound = ‘true’}
$ConfigurationSet = ‘true’
Write-Host "Found SID $ConfiguredSid"
}

#Act if Access Configuration is not enabled
if ($ConfigurationSet -eq ‘false’) {Write-Host "Access permission not configured"
$config = [xml] ‘<system.serviceModel.activation>
<net.tcp listenBacklog="10" maxPendingConnections="100" maxPendingAccepts="2" receiveTimeout="00:00:10" teredoEnabled="false">
<allowAccounts>
<add securityIdentifier="S-1-5-18"/>
<add securityIdentifier="S-1-5-19"/>
<add securityIdentifier="S-1-5-20"/>
<add securityIdentifier="S-1-5-32-544" />
</allowAccounts>
</net.tcp>
<net.pipe maxPendingConnections="100" maxPendingAccepts="2" receiveTimeout="00:00:10">
<allowAccounts>
<add securityIdentifier="S-1-5-18"/>
<add securityIdentifier="S-1-5-19"/>
<add securityIdentifier="S-1-5-20"/>
<add securityIdentifier="S-1-5-32-544" />
</allowAccounts>
</net.pipe>
<diagnostics performanceCountersEnabled="true" />
</system.serviceModel.activation>’

$configurationNode = $xmlDoc.DocumentElement
$newConfig = $xmlDoc.ImportNode($config.DocumentElement, $true)
$configurationNode.AppendChild($newConfig)

$allowAccounts = Select-Xml "configuration/system.serviceModel.activation/net.tcp/allowAccounts/add" $xmlDoc
$allowAccounts | ForEach-Object {
$ConfiguredSid = $_.Node.Attributes.Item(0).Value
Write-Host "Found SID $ConfiguredSid"
if ($ConfiguredSid -eq $UserSid) {$UserSidFound = ‘true’}
$ConfigurationSet = ‘true’
}

}

#Add Service User Sid if needed
if ($UserSidFound -ne ‘true’) {
$nettcp = $xmlDoc.SelectSingleNode("configuration/system.serviceModel.activation/net.tcp/allowAccounts")
$addNode = $xmlDoc.CreateElement(‘add’)
$secIden = $xmlDoc.CreateAttribute(‘securityIdentifier’)
$secIden.Value = $UserSid
$addNode.Attributes.Append($secIden)

$nettcp.AppendChild($addNode)
$xmlDoc.Save($SMSvcHostPathConfig)
Write-Host "Configuration Updated"
#Restart Service if running
if ((Get-Service NetTcpPortSharing).Status -eq "Running") {Restart-Service NetTcpPortSharing -Force}
}

[/code]

This script will search for the SMSvcHost.exe.config file, load it and check to see if the NAV Service User is already allowed access.  If not then the config file is updated and saved.  This script must be executed with administrative privileges.

Perhaps this should be what I started with, but the question; why do we need this, should be answered.

First, modifying the startup mode to delayed start is done to make sure that all the required networking and database processes have been started before the NAV Service starts.  This is very important if the SQL Server is running on the same server.  On a dedicated NAV Service server this is not as important but still recommended.

Secondly, accessing a NAV Service in most cases requires changes to a firewall.  Either to open a specific port or setting up a NAT from a public interface.  To minimize the number of ports used also minimizes the networking setup and maintenance.  If different network permissions or network access is required I recommend using separate ports for the NAV Services.

Please add block possibility to Permission table

Yesterday I suggested to Microsoft an enhancement to the permission functionality.  That was an informal suggestion so I logged into Microsoft Connect and added a formal suggestion.

In table 2000000005 Permission we can assign permission to objects.  In the Classic Client we had the possibility to assign permissions to objects with the type System.  This is not working in the NAV 2013 (R2) client.

This causes a problem, for example a user with SUPER (Data) permission can delete a company from the database.

I suggest that a new option be added to fields 6, 7, 8, 9, 10 in the above table.
Current option string is ” ,Yes,Indirect”
The new option string would be ” ,Yes,Indirect,Blocked”

If an access type is blocked in any permission entry the access will be blocked even if there is access in another permission set.

I would for example add a line to the Permission table for the SUPER (Data) permission set that will block Insert, Modify and Delete for table 2000000006 Company.

Please help me by voting for the suggestion.

Using a .dll proxy for web services

I have now completed my first all-dotnet codeunit.  The codeunit uses a dll file that I created from the web service WDSL.  This makes the programming a lot easier.

This solution has a BLOB fields that stores both incoming and outgoing xml.  When I use a proxy dll I don’t build a xml document and I never handle xml documents.  Again dotnet has a solution.  I use xml serializer from the system.xml object.

[code]LOCAL PROCEDURE SerializeToXMLStream@1100408008(VAR Object@1100408002 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Object" RUNONCLIENT;VAR NavOutstr@1100408004 : OutStream);
VAR
xmlSerializer@1100408000 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.Serialization.XmlSerializer" RUNONCLIENT;
StreamWriter@1100408003 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.StreamWriter" RUNONCLIENT;
File@1100408001 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.File" RUNONCLIENT;
NavInstr@1100408005 : InStream;
BEGIN
IF ISNULL(Object) THEN EXIT;
StreamWriter := StreamWriter.StreamWriter(TempFileName);
xmlSerializer := xmlSerializer.XmlSerializer(Object.GetType);
xmlSerializer.Serialize(StreamWriter,Object);
StreamWriter.Close;
UPLOADINTOSTREAM(”,MagicPath,”,TempFileName,NavInstr);
COPYSTREAM(NavOutstr,NavInstr);
File.Delete(TempFileName);
END;[/code]

I can send the proxy object to this function and will get the xml into the BLOB field.

[code]CurrencyRates := StatementService.GetCurrencyRates(TypeRates,CREATEDATETIME(RatesDate,235959T));

WITH BankAccAction DO BEGIN
"Modified by User ID" := USERID;
"Modification Date and Time" := CURRENTDATETIME;
"Incoming Message".CREATEOUTSTREAM(OutStr);
SerializeToXMLStream(CurrencyRates,OutStr);
MODIFY;
COMMIT;
END;[/code]

This example is executed on the client since I am using certificates and web services security for the communication.

A DotNet Interop Soap Web Request

I am currently working on a solution that requires a Dynamics NAV client to communicate with Dynamics NAV web service.  This I have done before with the classic client and have used automation objects for the job.  Now I wanted to do this with dotnet only objects in the Role Tailored Client.  Took some time to put all things together but here it is.  This version is running the request from the client.

 

Testing your Dynamics NAV Web Service

I am building a web service for one of my clients and another company is using this web service for an aspx web site.  I realized that I needed to test my web service before I can deliver it to that company.  So, I created a test codeunit for the job.

First I downloaded the universal XML import/export tool from Mibuso.  Then I added a function to the table 60000 XML Buffer that is in the above tool.
[code htmlscript=”false”]Read(VAR DOMDoc : Automation "’Microsoft XML, v6.0′.DOMDocument")
DELETEALL;
DOMNode := DOMDoc.documentElement;
Import2(DOMNode,1);
IF FINDFIRST THEN;[/code]
Next I create a read function in my test codeunit for every function in the web service.  Here is an example.
[code htmlscript=”false”]GetFarmerTankEntryAverageYW(FarmerID : Integer;MinYear : Integer;MaxYear : Integer;MinWeekNo : Integer;MaxWeekNo : Integer)
GetSetup;
CREATE(XMLDoc,TRUE,FALSE);

XMLProsInstr := XMLDoc.createProcessingInstruction(‘xml’,’version="1.0" encoding="utf-8"’);
XMLDoc.appendChild(XMLProsInstr);

CreateEnvelope(XMLElement1);
XMLElement2 := XMLDoc.createElement(‘soap:Body’);
XMLElement3 := XMLDoc.createElement(‘GetFarmerTankEntryAverageYW’);
XMLElement3.setAttribute(‘xmlns’,’urn:microsoft-dynamics-schemas/codeunit/RMWeb’);
CreateElement(XMLElement3, ‘farmerID’, FORMAT(FarmerID,0,9), ”, ”);
CreateElement(XMLElement3, ‘minYear’, FORMAT(MinYear,0,9), ”, ”);
CreateElement(XMLElement3, ‘maxYear’, FORMAT(MaxYear,0,9), ”, ”);
CreateElement(XMLElement3, ‘minWeekNo’, FORMAT(MinWeekNo,0,9), ”, ”);
CreateElement(XMLElement3, ‘maxWeekNo’, FORMAT(MaxWeekNo,0,9), ”, ”);
CreateElement(XMLElement3, ‘tankEntryXML’, ”, ”, ”);
XMLElement2.appendChild(XMLElement3);
XMLElement1.appendChild(XMLElement2);
XMLDoc.appendChild(XMLElement1);

WinHTTP.open(‘POST’,ServiceURL,FALSE,UserName,Password);
WinHTTP.setRequestHeader(‘Content-Type’,’text/xml; charset=utf-8′);
WinHTTP.setRequestHeader(‘SOAPAction’,’GetFarmerTankEntryAverageYW’);
WinHTTP.send(XMLDoc);

IF WinHTTP.status <> 200 THEN
ERROR(Text003,WinHTTP.status,WinHTTP.statusText);

XMLResponseDoc.load(WinHTTP.responseXML);
DisplayDocument(XMLResponseDoc);[/code]
This will use the XML Buffer to read the response document and display the result.  The Text Constant Text003 contains
[code htmlscript=”false”]ENU=Status error %1 %2;ISL=Stöðuvilla %1 %2[/code]
and the four functions used here contain
[code htmlscript=”false”]DisplayDocument(VAR XMLDoc : Automation "’Microsoft XML, v6.0′.DOMDocument")
XMLBuffer.Read(XMLDoc);
COMMIT;
FORM.RUNMODAL(FORM::"XML Buffer");

CreateEnvelope(VAR InElement : Automation "’Microsoft XML, v6.0′.IXMLDOMElement")
InElement := XMLRequestDoc.createElement(‘soap:Envelope’);
InElement.setAttribute(‘xmlns:soap’,’http://schemas.xmlsoap.org/soap/envelope/’);
InElement.setAttribute(‘xmlns:xsi’,’http://www.w3.org/2001/XMLSchema-instance’);
InElement.setAttribute(‘xmlns:xsd’,’http://www.w3.org/2001/XMLSchema’);

CreateElement(VAR InElement : Automation "’Microsoft XML, v6.0′.IXMLDOMElement";InNodeName : Text[50];InNodeValue : Text[250];InAttribu
TempElement := XMLRequestDoc.createElement(InNodeName);
TempElement.nodeTypedValue(InNodeValue);
IF InAttributeName <> ” THEN
TempElement.setAttribute(InAttributeName,InAttributeValue);
InElement.appendChild(TempElement);

GetSetup()
ServiceURL := ‘http://gunnar.dynamics.is:7047/DynamicsNAV/WS/CRONUS/Codeunit/WebService’;
UserName := ‘<Domain\User>’;
Password := ‘<Password>’;
IF ISCLEAR(WinHTTP) THEN
CREATE(WinHTTP,TRUE,FALSE);
IF ISCLEAR(XMLResponseDoc) THEN
CREATE(XMLResponseDoc,TRUE,FALSE);[/code]

WinHTTP and RTC Client

I have been using the automation ‘Microsoft XML, v6.0’.XMLHTTP to communicate with web services and web sites.  I have been experiencing a problem with this automation when running in Role Tailored Client.  The solution has been to use the automation ‘Microsoft XML, v6.0’.ServerXMLHTTP when running in the service tier.
[code htmlscript=”false”]IF ISSERVICETIER THEN BEGIN
IF ISCLEAR(WinHTTPServer) THEN
CREATE(WinHTTPServer,TRUE,FALSE);
WinHTTPServer.open(‘GET’,URL,FALSE);
WinHTTPServer.send(”);

IF WinHTTPServer.status <> 200 THEN
ERROR(Text007,WinHTTPServer.status,WinHTTPServer.statusText);

DOMDocument.load(WinHTTPServer.responseXML);
CLEAR(WinHTTPServer);
END ELSE BEGIN
IF ISCLEAR(WinHTTP) THEN
CREATE(WinHTTP,TRUE,FALSE);
WinHTTP.open(‘GET’,URL,FALSE);
WinHTTP.send(”);

IF WinHTTP.status <> 200 THEN
ERROR(Text007,WinHTTP.status,WinHTTP.statusText);

DOMDocument.load(WinHTTP.responseXML);
CLEAR(WinHTTP);
END;[/code]
Where Error string Text007 is “Status error %1 %2”.

WSDL Code Generator

On several occasions I have needed to create a code to communicate with Soap Web Services. The manual labor in creating the functions and XML Ports is something that I would like to be rid of. On that node I created a Batch that does most of the job for me.

Execute this and you will be asked to save the “Web Service Objects.txt” file to your computer and from there you will be able to import the file and continue the work.

You will get XMP Ports for every method, both the request and response.  You will also get a Codeunit with a function for every method and the necessary functions to handle the web service communication.

What is left is for you to connect your data to the functions and the XML Ports.

This is not fully tested, so any updates would be appreciated.

Import WSDL (Updated 2012-12-08)

NAV Web Service and PHP

I wanted to replace MySQL with NAV Web Service as a data source for my PHP web site.  I started to look around and found all I needed in Freddys Blog.  First post is about changes in authentication method of the web service, the later post displays the code needed to connect to the web service with php.

I have an php web on my Windows Home Server and needed to activate two more extensions to get things working.

Here are the php files that I used – PHP consume NAV Web Service