TL; DR;
Explains the creation of a simple, self-hosted, RESTful web service made with WCF, that runs in client computers in order to allow a web application to control local peripherals (like scanners, printers, etc.), without resorting to the use of browser plugins.
Introduction
Today, every new development must take into account the web paradigm, no matter if it’s about games, enterprise systems, collaborative tools or social applications.
The web platform is more mature right now than it was, for instance, 5 years ago. But still, there are some things that are difficult to implement in a pure web environment which are not so in a desktop application.
For example, if you have to control a peripheral like a scanner, a web application may have no control or even knowledge of it because as you know, browsers cannot access hardware directly.
The first approach one may think in order to overcome these limitations is: browser plugins. To me, those are a last-resort solution.
Browser plugins may be used to bridge the gap between web applications and everything else that is not part of the web domain, and this may seem as an advantage, but let’s see the disadvantages:
- They must support the underlying operating system architecture. A plugin made for a Windows browser doesn’t work in OS or Linux.
- They must support the browser type itself. A plugin made for Firefox doesn’t work on Internet Explorer and vice-versa.
- Plugins may hang the browser if they’re buggy.
- Whenever a new version of the plugin is released, a way to deploy updates must be taken into account.
- Some browser configurations may require signed plugins.
If plugins aren’t the way to go, we must think of an alternative approach that help us fill this functionality gap.
Motivation of creating this service
The idea of building this service emerged because of a need to provide a web-based document management system (DMS) the capability to manage a virtual printers (VP) installed in user computers. These VPs are used to capture print jobs coming from any installed applications and send the job pages as PDF documents directly to the DMS web server for its processing and storage.
A solution that could integrate platform-native functionality (like handling a VP) into a web application, but at the same time could leave the implementation details in a separate component would be a big advantage over other approaches like browser plugins.
The proposed solution embraces:
- Modularity: by separating the platform-native functionality from the web app.
- Maintainability: by coding one time for any browser (of the same platform, at least).
- Cleaner code base: by using pure Ajax-style web service calls from the web app instead of using the <object> and <embed> tags as we would if resorting to browser plugins.
- Stability: by not messing with the browser, at least not directly.
The figure below compares the plugin vs. the self-hosted service approach in order to add platform-native functionality to a web application:
Fig.1 Application that depends on browser plugins
Fig.2 Plugin-less application
Background info: How the local service works
The DMS web client communicates with the local web service, this service configures the virtual printer installed in the computer, and this in turn sends captured jobs to a remote location (the DMS back-end web service).
To get a better picture, take a look at the following image and the description:
Fig. 3
- The client logs in the DMS web app by sending user/password.
- After logging in, two pieces of information are sent from the browser to the local self-hosted service via JavaScript –ajax- calls:
- The user's login name, which will be used to identify a virtual bin in the DMS server.
- The endpoint address of the DMS web service. This is vital, since the local service doesn’t know -and it’s not hardcoded with- the DMS web service address. This endpoint will be used by the local service to send the images captured by the virtual printer to the back-end.
- The local web service installs a virtual printer when it starts.
- When any application installed in the user’s computer prints to this virtual printer, it captures the jobs and converts the EMF data to a PDF document.
- The local service sends the document to the DMS back-end, right into the user’s virtual bin. When the transfer is complete, the user is notified via a Windows taskbar balloon notification.
-
- The user opens the virtual bin from inside the DMS web client -where the captured files should be shown-, and
- After selecting the required files, these are added to the corresponding location (folder, document, etc.) in the DMS and removed from the virtual bin.
This approach gives an additional input to the standard “file upload/import” capability of classic web applications: print-to-web-service.
Even when platform-specific code must be written for the local service, the disadvantages #2, #3 and #5 listed in the introduction are completely eliminated. And with the help of some more clever code, an auto-updatable service could also be coded, but that goes beyond the scope of this article.
Why WCF instead of Web Api
WCF is a very versatile framework to build any kind of web services. Although it may seem it was designed with SOAP/WSDL in mind, it allows building RESTful services too.
But the main reasons why I chose WCF over Web Api are:
- I know WCF since a long time now, and I wanted to take advantage of that knowledge when designing this solution.
- Although this article shows a simplified service, this is an adaptation of a real-world WCF service that's already running, so I just wanted to add a REST endpoint (to simplify JavaScript communication) without drastically changing the whole solution’s architecture.
- Because it was fun to try!
Setting up WCF for REST
Here is a basic list of what is needed in order to configure a WCF service for REST:
- Define a class that implements the web service’s functionality
- Define the service’s ABC (Address, Binding, Contract)
- Mark the service and its methods with attributes:
- Mark the service class with the ServiceContract
attribute
- Mark the service class with the ServiceBehavior
attribute
- Mark the service methods with the OperationContract
attribute
- Mark the service methods with the WebGet or WebInvoke attributes
- Prepare the service to support CORS (Cross-origin resource sharing)
- Instantiate and expose the service via the WebServiceHost class
Now, let’s explain each part…
Define a class that implements the web service’s functionality
The traditional approach when working with WCF services is to define an interface that will be published and then create a class that implements that interface.
But since this service is very simple, we created the class without any prior interface definition. Later we will obtain the contract to be published directly from this class by using the ContractDescription
class located in the System.ServiceModel.Description
namespace.
This is the skeleton version of the self-hosted service. It has only two methods; one to obtain the name of the virtual bin, and the other to configure it along with the remote endpoint of the DMS web service. Since one method is read-only it will be accessed via HTTP GET, and the other one via HTTP POST since it sends information to the service (more on this later).
Public Class PTWService
Private _wsep as String
Private _bin as String
Public Sub New()
End Sub
Public Function GetUserBin() As String
Return _bin
End Function
Public Sub SetConnectionInfo (ServiceEndPoint As String, UserBin As String)
_bin = UserBin
_wsep = ServiceEndPoint
End Sub
End Class
Define the service’s ABC (Address, Binding and Contract)
Address: Since this service will be accessed only from the same computer where the DMS web client runs, the 127.0.0.1 IP address is used.
Another thing that we must supply is the port of the service. We cannot use well-known ports, like 80, 8080, 443, or so because they may conflict with existing services running in the computer, so we picked a port with number 8990.
Binding: This part is very important. WCF supports several types of bindings out of the box, and one of them suits our needs without any further configuration: WebHttpBinding. This binding is used for services that need to be exposed through HTTP instead of SOAP.
Contract: As explained earlier, the contract will be obtained directly from our service class with the help of a framework class called ContractDescription
.
Mark the service and its methods with attributes
Mark the service class with the ServiceContract
attribute: This is a basic requirement when writing any service with WCF.
Our service class definition now looks like this:
<ServiceContract(Namespace:="http://www.myselfhostedservice.com")>Public Class PTWService
Mark the service class with the ServiceBehavior
attribute: This attribute specifies how the service will be instantiated and accessed. In our example, the service will allow multiple simultaneous calls (ConcurrencyMode = multiple)
, and all of them will be directed to a single instance object (InstanceContextMode = single)
, a.k.a. a Singleton.
So the class signature now looks like this:
<ServiceBehavior(ConcurrencyMode:=ConcurrencyMode.Multiple, IgnoreExtensionDataObject:=True, InstanceContextMode:=InstanceContextMode.Single, AddressFilterMode:=AddressFilterMode.Any), ServiceContract(Namespace:="http://www.myselfhostedservice.com")>
Public Class PTWService
Mark the service methods with the OperationContract
attribute: The same as with the ServiceContract
attribute, this is a basic requirement of any WCF service.
We only set one parameter of the OperationContract
attribute: “Action”, that will indicate WCF to dispatch messages with the specified action name to a specific service method:
<OperationContract(Action:="GetUserBin")>
Public Function GetUserBin() As String
Mark the methods with the WebGet or WebInvoke attributes: This is a key part in the service setup for REST. It will configure the service methods to handle the different HTTP verbs: GET, POST and OPTION; the latter used to enable cross origin resource sharing (CORS).
The services methods will now look like this, with all the attributes set in place:
<WebGet(UriTemplate:="GetUserBin",
RequestFormat:=WebMessageFormat.Json, ResponseFormat:=WebMessageFormat.Json)>
<OperationContract(Action:="GetUserBin")>
Public Function GetUserBin() As String
AddCorsHeaders(False)
Return _bin
End Function
<WebInvoke(Method:="OPTIONS", UriTemplate:="SetConnectionInfo")>
<OperationContract(Action:="SetConnectionInfo")>
Public Sub SetConnectionInfo_Options()
AddCorsHeaders(False)
WebOperationContext.Current.OutgoingResponse.StatusCode = Net.HttpStatusCode.OK
End Sub
<WebInvoke(Method:="POST", UriTemplate:="SetConnectionInfo", BodyStyle:=WebMessageBodyStyle.WrappedRequest, RequestFormat:=WebMessageFormat.Json, ResponseFormat:=WebMessageFormat.Json)>
<OperationContract(Action:="SetConnectionInfo")>
Public Sub SetConnInfo_Post(ServiceEndPoint As String, UserBin As String)
_wsep = ServiceEndPoint
_bin = UserBin
AddCorsHeaders(False)
WebOperationContext.Current.OutgoingResponse.StatusCode = Net.HttpStatusCode.OK
End Sub
Private Sub AddCorsHeaders(bOnlyOrigin As Boolean)
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*")
If bOnlyOrigin = True Then
Return
End If
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "POST, PUT, DELETE")
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept")
End Sub
The WebGet
attribute tells WCF that the method GetUserBin
will be accessed by the HTTP GET verb, which in turn -following the REST recommendation- should be used when a method call implies NO change in the service’s state (aka read-only methods).
The UriTemplate
parameter of the WebGet
attribute is used by WCF to dispatch the messages to the appropriate service methods. In the case of our GetUserBin
method, there is nothing really particular with it, but it will have more meaning when we explain the WebInvoke attribute later.
The other two parameters, RequestFormat
and ResponseFormat
, are both set to WebMessageFormat.Json
, and those configure the service to accept parameters encoded as strings in JSON format, and the response, if any, will be sent as JSON too.
As you may have noticed, there are two methods flagged with the WebInvoke attribute that share the same UriTemplate, but have different “Method” parameter values. These methods work together to enable CORS.
Prepare the service to support CORS (Cross-origin resource sharing)
Whether or not to enable CORS depends on how you will access your services. In this particular case, since the web application will be served from an origin different than the self-hosted service, CORS needs to be set up or the calls to the self-hosted service will fail.
A little background on CORS:
For security reasons, web pages are allowed to access restricted resources only when they come from the same origin as the pages themselves. This restriction applies to web fonts and XML HTTP Requests (XHR), which are the core of Ajax.
Since the self-hosted service runs in the local computer with an IP of 127.0.0.1, chances are that this will NOT be the same origin as the pages served by the DMS web server.
So, when one of these pages makes an Ajax request to a resource that is not from the same origin as the page itself (the local computer), it fails.
To support this scenario there is a mechanism called CORS, which allows accessing resources that are outside the page domain.
CORS works with the user agent (browser). When it detects a request to a foreign resource, for example an XHR wants to get some JSON in another domain, it first sends an HTTP OPTIONS message and expects a specific response (approval) from the server. If the response is received, then the original HTTP request is sent (GET, POST, etc.)
A deep explanation about CORS would not fit in here, so I recommend reading these resources:
http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
http://blogs.msdn.com/b/carlosfigueira/archive/2012/05/15/implementing-cors-support-in-wcf.aspx
Now back to the service methods, there are two of them that share the same UriTemplate:
SetConnectionInfo_Options
SetConnectionInfo_Post
The reason for this is that both represent different phases of the same call.
The first is flagged with WebInvoke, and the specified HTTP verb is “OPTIONS”. This means that when a request tries to contact our service by calling SetConnectionInfo
, and the requester is in a different web domain than the service, the browser will first send an HTTP OPTIONS message to the address SetConnectionInfo
. This message will be caught by WCF and routed to the internal method SetConnectionInfo_Options because it has the WebInvoke attribute configured to handle the HTTP OPTIONS verb.
This method will internally call another one, called AddCorsHeaders
, which will write some HTTP headers right into the response stream in order to enable CORS:
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*")
If bOnlyOrigin = True Then
Return
End If
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "POST, PUT, DELETE")
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept")
When the browser receives the response to this first message, it will send a second request to SetConnectionInfo
with the original, intended data. This time, WCF will check that is an HTTP POST, and routes the request to the internal SetConnectionInfo_Post
method.
Instantiate and expose the service via the WebServiceHost class
The last step in our service setup is to instantiate the service class, and start listening to requests.
The service is instantiated by a method called StartClientListener
in a helper class called WebServiceManager
, which is a singleton object. The method goes as follows:
Private _host As WebServiceHost
Public Function StartClientListener(port As Integer, ByRef err As Exception) As Boolean
If _host IsNot Nothing Then
err = New ApplicationException("Host already started")
Return False
End If
Dim go As New Threading.ManualResetEvent(False)
Dim createEx As Exception
Threading.ThreadPool.QueueUserWorkItem(Sub()
Try
Dim bnd As New WebHttpBinding
Dim ep As ServiceEndpoint
_host = New WebServiceHost(GetType(PTWService))
Dim cd As ContractDescription = ContractDescription.GetContract(GetType(PTWService))
Dim ea As New EndpointAddress("http://127.0.0.1:" & port.ToString & "/PTWService")
ep = New ServiceEndpoint(cd, bnd, ea)
_host.AddServiceEndpoint(ep)
_host.Open()
Catch ex As Exception
createEx = ex
End Try
go.Set() 'signals calling thread that host setup is done.
_exitFlag.WaitOne() 'waits until the exit signal is received.
End Sub)
go.WaitOne() 'waits until the host setup is completed.
Try
If createEx IsNot Nothing Then
Throw createEx
End If
Return True
Catch ex As Exception
Try
_exitFlag.Set()
_host.Close()
Catch
End Try
_host = Nothing
err = ex
Return False
End Try
End Function
What this method does is:
- Creates a WebHttpBinding binding, which is used to expose the service as REST instead of SOAP.
- Creates a reference to the web service host. The parameter passed to the WebServiceHost’s constructor is the type of our service class.
- Uses the framework’s
ContractDescription
class in order to get the contract to be published by WCF via the web service host. - Creates an
EndPointAddress
for the service, using the IP 127.0.0.1 and a custom port. - With the endpoint address, the binding and the contract description (ABC), it creates a ServiceEndpoint object that is added to our host.
- Finally, the host is opened, so it can listen to client requests.
Since all that code is run in an anonymous method delegated to the ThreadPool, the go wait-handle is used to signal the calling thread that the host setup is done.
One important thing to note is that the code that opens the host should be asynchronous! One common pitfall is to create a ServiceHost or WebServiceHost object and open it in the UI thread, when using Winforms.
These classes detect the context and will use the UI thread if it's available to attend request, thus blocking responsiveness of Winforms programs! The exception to this would be creating the host before starting the UI thread, for example in a main.cs or main.vb file that, after opening the host, calls Application.Run(form)
.
As a final note, here is an example of a JavaScript function which calls the local service (JQuery needed!):
var wsdata = JSON.stringify({ ServiceEndPoint: $("#_wsep").val(), UserBin: $("#_binName").val() });
jQuery.support.cors = true;
$.ajax({
type: "POST",
url: "http://localhost:8990/PTWService/SetConnInfo",
contentType: "application/json; charset=UTF-8; charset-uf8",
data: wsdata,
cache: false,
timeout: 2000,
success: function () {
alert("Bin configured!");
},
error: function (jqXHR, textStatus, errorThrown) {
alert("Ooops!");
}
});