One of the new features introduced with PowerBuilder.Net 12.5 was the ability to create WCF web services. The version of the product also introduced a client for REST web services as well, and a WCF client had been introduce in an earlier version. One frequent question I heard when presenting the new features in conference or online sessions was when PowerBuilder.Net would provide the capability to create REST services, not just consume them.
Perhaps what few people realized (including myself at the time) is that WCF web services isn't just for creating SOAP services. Since .Net 3.0, they have been capable of creating REST services as well. So we've actually have had the capability to create REST web services with PowerBuilder.Net since 12.5 was released. In this blog post we'll look at how we do that.
The first thing we need to do is go ahead and create a WCF soap web service. We're going to use pretty much the same approach that is demonstrated in this SAP D&T Academy video. One difference is that I'm just going to use an ODBC data source for this sample. In the video I used an ADO.Net datasource, which is the better approach for anything more than a demo when using .Net targets.
As in that video, I have a datawindow that selects the employees from the EAS demo database. I also have a structure that has the same configuration as the result set of the datawindow, so transferring the data to and array of that structure can be accomplished through a one line dot notation call. The code for the function that retrieves the employees looks like this.
- DataStore lds
- long ll_rc
- s_structure emps[]
- Transaction ltrans
- ltrans = create Transaction
- // Profile EAS Demo DB V125
- ltrans.DBMS = "ODBC"
- ltrans.AutoCommit = False
- ltrans.DBParm = "ConnectString='DSN=EAS Demo DB V125 Network Server;UID=dba;PWD=sql'"
- //ltrans.DBParm = "ConnectString='DSN=EAS Demo DB V125 - 64 bit;UID=dba;PWD=sql'"
- connect using ltrans ;
- if ltrans.SQLCode <> 0 then
- emps[1].emp_fname = "Connect failed: " + ltrans.SQLErrText
- else
- lds = create DataStore
- lds.DataObject = 'd_grid'
- ll_rc = lds.SetTransObject ( ltrans )
- if ll_rc <> 1 then
- emps[1].emp_fname = "SetTransObject failed: " + string ( ll_rc )
- else
- ll_rc = lds.Retrieve()
- if ll_rc < 0 then
- emps[1].emp_fname = "Retrieve failed: " + string ( ll_rc )
- else
- emps = lds.Object.Data
- end if
- disconnect using ltrans ;
- end if ;
- destroy ltrans
- end if ;
- return emps
I'm running the EAS Demo database in network server mode with TCP enabled as a communications method so that the web service can connect to an already running database.
At this point we can go into the project painter for the service, select that function to be exposed in the service and run the project (having specified the wfcservice_host.exe file in the webservice.out/bin/Debug directory as what is run when we run the project). We'll see a command prompt window display, and we should be able to access the WSDL for the SOAP service and create a WCF client for it.
Once we know that part is working, we're going to make a few modifications to make it a REST service instead. The first thing we're going to do is go back into the project painter, select the method we're exposing and then click on the operational attributes button. Note that the button won't be enabled until you select a method that you want to adjust the attributes for.
Within the dialog that appears, select the WebInvoke Attribute category. Within that category, set the Method to Get and provide a UriTemplate. In a REST web service, the method is called by adding the UriTemplate onto the end of the root URL for the service. So in this case, since the service URL is:
The method for retrieving the employees becomes:
Normally in REST services, a GET method is mapped to a retrieve, PUT to an insert, POST to an update, and DELETE deletes. Arguments to the method are additional entry on the URL. For example, we could create a method that returns a single employee record and takes the employee id as an argument. If the employee id was 123, then the method URL might look like this:
Unless we specify a specific ResponseFormat and RequestFormat, XML is assumed (same as the SOAP service). REST services are more likely to return JSON though, as it is not as verbose. We can tell WCF that we want JSON returned instead by specifying that for the ResponseFormat.
We're done with the service project. What we need to do now is make some changes to the <projectname>.config file in the root directory. First, we need to find the endpoint address entry for the service and change the binding from basicHttpBinding to webHttpBinding. We're also going to add a behaviorConfiguration attribute and give it a name. We'll define that a bit later in the same file.
Original File:
- <system.serviceModel>
- <services>
- <service name="Sybase.PowerBuilder.WCFNVO.n_customnonvisual"
- behaviorConfiguration="ServiceNameBehavior">
- <endpoint address=""
- binding="basicHttpBinding"
- contract="Sybase.PowerBuilder.WCFNVO.n_customnonvisual"
- bindingNamespace="http://tempurl.org" />
Revised File
- <system.serviceModel>
- <services>
- <service name="Sybase.PowerBuilder.WCFNVO.n_customnonvisual"
- behaviorConfiguration="ServiceNameBehavior">
- <endpoint address=""
- binding="webHttpBinding"
- contract="Sybase.PowerBuilder.WCFNVO.n_customnonvisual"
- bindingNamespace="http://tempurl.org"
- behaviorConfiguration="EndpointNameBehavior" />
There should already be a serviceBehaviors section in the file within the behaviors section. What we're going to do is add a endpointBehaviors section to the file as well below the serviceBehaviors. Give it the same name as you referenced in the new attribute for the endpoint earlier. The only thing we need to include it in is the webHttp attribute:
- </serviceBehaviors>
- <endpointBehaviors>
- <behavior name="EndpointNameBehavior">
- <webHttp />
- </behavior>
- </endpointBehaviors>
- </behaviors>
With that, we're done. Redeploy the project so that all of the new settings apply. You should now be able to open a browser and give it the base URL plus the URITemplate. If all is working correctly, you should see JSON being returned.
One of the downsides of REST services that there really isn't a way to automatically generate documentation for how the service operates, like the WSDL for a SOAP operation. You're going to have to develop documentation by hand to let users know how to consume the service.
Now let's look at a method that has arguments. We're going to create a method that takes a single argument, the emp_id, and returns that employee. If you have more than one argument, you'll just extend the technique we use here for a single argument.
The code that retrieves a single employee is a slight modification of the code that returns them all:
- DataStore lds
- long ll_rc
- s_structure emp
- Transaction ltrans
- ltrans = create Transaction
- // Profile EAS Demo DB V125
- ltrans.DBMS = "ODBC"
- ltrans.AutoCommit = False
- ltrans.DBParm = "ConnectString='DSN=EAS Demo DB V125 Network Server;UID=dba;PWD=sql'"
- //ltrans.DBParm = "ConnectString='DSN=EAS Demo DB V125 - 64 bit;UID=dba;PWD=sql'"
- connect using ltrans ;
- if ltrans.SQLCode <> 0 then
- emp.emp_fname = "Connect failed: " + ltrans.SQLErrText
- else
- lds = create DataStore
- lds.DataObject = 'd_grid2'
- ll_rc = lds.SetTransObject ( ltrans )
- if ll_rc <> 1 then
- emp.emp_fname = "SetTransObject failed: " + string ( ll_rc )
- else
- ll_rc = lds.Retrieve( Integer ( empid ) )
- if ll_rc < 0 then
- emp.emp_fname = "Retrieve failed: " + string ( ll_rc )
- else
- emp = lds.Object.Data[1]
- end if
- disconnect using ltrans ;
- end if ;
- destroy ltrans
- end if ;
- return emp
What's not obvious from the code is that the emp_id we're taking as an argument is of type string, and we're converting it to an integer within the code. We have to pass all of the arguments to the function as string values, and deal with converting to the appropriate data type within the method because all of the arguments passed in on a URL reference are considered to be of string data type.
Lets look at the way we set up the WebInvoke configuration for this operation, and you'll see another difference:
Note that the UriTemplate is now:
employee/{empid}
That means that the method is expecting to be in the form we mentioned earlier where the argument is obtained from part of the URL itself, In particular:
The {empid} indicates where an argument will occur and what name it has in the underlying method. If you've created REST web service clients in PowerBuilder.Net, you should be somewhat familiar with that type of approach.
While this works well when there is only one argument, it's not ideal when a number of arguments need to be passed. In that case, we can use an alternative UriTemplate approach which uses variable name value pairs:
And the URL used to retrieve a single employee would then be:
Multiple arguments follow along with a & between them.
For a detailed discussion of how UriTemplates are used, see the "Designing the UreTemplates" section of this guidance from Microsoft:
One last note. ODBC profiles are unique between 32 and 64 bit apps. PowerBuilder.Net is 32 bit, and the WCF service will (if run on a 64 bit machine) be running as a 64 bit app. That means the service would need to use a different ODBC profile than the one I used to develop the app. Further, PowerBuilder.Net has an easier time debugging the WCF service if it's running as 32 bit rather than 64 bit. Therefore, I actually wanted the WCF service host to run as 32 bit rather than 64 bit for development and debugging.
To accomplish that, I copied the wcfservices_host.exe file in the output folder and renamed the copy to wcfservices_host32.exe. I then ran corflags on it to change it to a 32 bit application. I copied the wcfservices_host.exe.config file that PowerBuilder generated and renamed it to match the new executable name. Next, I marked them both read only so PowerBuilder wouldn't delete them the next time I deployed the app. Finally, I modified the service project so it ran the 32 bit executable whenever I wanted to run or debugged the service.
No comments:
Post a Comment