Practical .NET

Writing a WCF 4.5 WebSocket Service

Peter Vogel continues his exploration of WCF 4.5's support for WebSockets by writing the code to accept data from the client and then return data to the client whenever that data becomes available.

In my last column, I discussed why I thought WebSockets was so important and laid the groundwork for creating a WebSocket service using WCF 4.5, Visual Studio 11 and Windows Server 8. In this column, I'll walk through the code for the service I started in the last column. I'll use the standard WCF paradigm of defining a set of interfaces, then implementing them.

A WebSockets service consists of two interfaces: one holding the method that receives the client's requests, and another used to send results to the client. Your WCF must implement the first interface—the one with the method that accepts requests.

In my example, I've defined an interface called IRequestOrderStatusData with a method called OrderStatusById that allows the client to request status updates for an order by passing an Order Id to the method. I add a WCF service called OrderStatus to my WCF Service Application and have it implement that interface:

Public Class OrderStatus
    Implements IRequestOrderStatusData
Opening the Connection
The method in this interface is first called when the client opens the connection to the service (made through an HTTP request that allows WCF to set up the TCP channel used for all subsequent requests). No parameters are passed when the connection is opened, so at the top of the method, you'll need to see if the parameter passed to the method (which must be a Message object) is empty before attempting to process any parameters. You can do that with the Message object's IsEmpty method:

Public Sub OrderStatusById(OrderIdMessage As Channels.Message) 
      Implements IRequestOrderStatusData.OrderStatusById

  If Not OrderIdMessage.IsEmpty Then

While this forces the client to always pass a parameter when calling the service's method to distinguish between that opening call and calls to the method, the method's parameter isn't optional when calling the method so this technique works reliably.

Just because no parameter is passed when the connection is opened doesn't mean the client can't pass information to the service when opening the connection. The client uses the URL for your service when opening the connection to your service and can include information in the query string of that URL. While the Message object passed when the connection is opened doesn't contain the method's parameter, the object does allow you to access the querystring information passed during the opening process.

The first step in accessing the querystring is to retrieve the WebSocketMessageProperty object passed in the parameter's Properties collection. Once you have it, you can retrieve its WebSocketContext object, which will let you access, through its RequestUri property, the URL the client used to open the connection to your service.

This code uses the HttpUtility's ParseQueryString to break the querystring (in the RequestUri's Query property) into a dictionary of name value pairs and retrieves the value for the City name from the querystring:

Dim wsProp As Channels.WebSocketMessageProperty =
  CType(OrderIdMessage.Properties("WebSocketMessageProperty"), 
        Channels.WebSocketMessageProperty)
Dim wsContext As Net.WebSockets.WebSocketContext = 
        wsProp.WebSocketContext
Dim parms = 
      HttpUtility.ParseQueryString(wsContext.RequestUri.Query)
Dim city As string = parms("City")
Handling Requests
Once the client has opened the TCP connection to your service, it can start making calls to your service's method, passing a single parameter value. To retrieve the parameter value passed to your method, you'll need to use the Message object's GetBody method, which will return an array of bytes. I'm assuming that I want to work in an interoperable mode, so I want to use String values when passing data between the client and the server. This code converts the GetBody's array of bytes into a string for later processing:

Dim bytOrderId As Byte()
Dim OrderId As String
bytOrderId = OrderIdMessage.GetBody(Of Byte())()
OrderId = Encoding.UTF8.GetString(bytOrderId)

I'll pass over the processing that retrieves the Order's status and assume it's been retrieved and is ready to be sent to the client. To send data to the client, you need to open a connection to the client using the interface you designed for returning the data. It makes sense to me to create that connection in the service's constructor and store it in a field for the service (if your service may not always return a value, you can defer opening the channel until you need it). If you want, you can even create the connection on each transmission, though I'm not sure what impact that will have on performance.

This code opens a channel in the constructor using the ISendOrderStatus interface defined earlier, and stores the channel in a field:

Private SendOrderStatusData As ISendOrderStatus

Public Sub New()
  SendOrderStatusData = 
    OperationContext.Current.GetCallbackChannel(Of ISendOrderStatus)()
End Sub

At this point you're ready to send some data to the client using the method defined in your interface (in my case, that's a method called SendOrderStatus). You have to pass the method a Message object. Creating a Message object is a multi-step process, so it makes sense to create a utility routine to handle that.

I'm still assuming that I want to work in an interoperable, so I still want to return a String value. The first step in creating a message that holds a string is to convert the string data into a byte array. Once you've done that you must instantiate an ArraySegment of bytes, passing your byte array:

Public Shared Function CreateStringMessage(Content As String)
               As Channels.Message
  Dim bytContent As Byte()
  bytContent = Encoding.UTF8.GetBytes(Content)
  Dim asContent As ArraySegment(Of Byte)  
  asContent = New ArraySegment(Of Byte)(bytContent)

With the ArraySegment create you can create a Message object by passing the ArraySegement to the ByteStreamMessage object's CreateMessage method (you'll need to add a reference to the System.ServiceModel.Channels to use the ByteStreamMessage object):

Dim msg As Channels.Message
msg = Channels.ByteStreamMessage.CreateMessage(asContent)

However, if you're returning a String, you must add a WebSocketMessageProperty object to your Messages's Properties collection. That WebSocketMessageProperty object must have its MessageType property set to WebSocketMessageType.Text and must be added to the Properties collection under the key WebSocketessageProperty. That's what this code does before returning the Message object:

msg = WebSocketsUtlities.CreateStringMessage(OrderStatusData)
Dim wsmProperty = New Channels.WebSocketMessageProperty
wsmProperty.MessageType = Net.WebSockets.WebSocketMessageType.Text
msg.Properties("WebSocketMessageProperty") = wsmProperty
Return msg

The client can close the connection that your service is using to send data. If so, your connection will be disposed (this is another good reason to create the connection once at the start of the service). It's a good practice to check that your connection still exists before returning data.

Code to convert a String into a Message using my utility and send the resulting Message to the client would look like this:

Dim OrderStatusMessage As Channels.Message
OrderStatusMessage = 
  WebSocketsUtilities.CreateStringMessage(OrderStatusJson)
Try
  SendOrderStatusData.SendOrderStatus(OrderStatusMessage)
Catch
  Exit Sub
End Try

One of the benefits of using WebSockets is that you're not restricted to sending a single status update. Your service can continue to monitor the Order's status and send an update to the client whenever the service feels it's necessary.

Configuration Changes
In order for this to work, however, you'll need to tweak the bindings for your service in your config file. To support WebSockets, WCF 4.5 provides a new message encoding format which you'll need to specify in a cutomBinding element in your config file's bindings element. Your customBinding element must include the byteStreamMessageEncoding element.

You only need one binding that specifies these settings, so you might as well give a generic name (I used webSocket) and tie it to all of your WebSocket services in your project. In your service's configuration, you'll need to set its binding attribute to customBinding and set the bindingConfiguration to the name of the customBinding  you created.

Here are the settings used for my service:

<services>
  <service name="WSSample.OrderStatus">
    <endpoint address="" 
           binding="customBinding" 
           bindingConfiguration="webSocket"
           contract="WSSample.IRequestOrderStatusData" />
  </service>
</services>

<bindings>
  <customBinding>
    <binding name="webSocket">          
      <byteStreamMessageEncoding/>            
      <httpTransport>            
        <webSocketSettings transportUsage="Always" 
                createNotificationOnConnection="true"/>
      </httpTransport>
    </binding>
  </customBinding>
</bindings>
In my next column, I'll create the JavaScript client that will call this service. I'll also discuss some patterns for using a WebSockets-based service.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube