May 2018

Volume 33 Number 5

[Cutting Edge]

Under the Covers of ASP.NET Core SignalR

By Dino Esposito

Dino EspositoSignalR is the latest addition to the ASP.NET Core platform, and a long-awaited one at that. I dare say that only now with SignalR on board can we really start looking at ASP.NET Core as ready for prime time for all types of applications. No serious Web application today can do without some form of asynchronous notification and real-time capabilities.

The SignalR you know from ASP.NET has been totally rewritten and resides now in the family of ASP.NET Core libraries. In my column last month (msdn.com/magazine/mt846469), I offered a quick tour of the features and capabil­ities of the new ASP.NET Core SignalR. In this column, I’ll dive into the internal machinery.

SignalR is an abstraction layer for bidirectional, two-way remote procedure calls (RPC) and works over a variety of transport protocols. It’s host-agnostic and not limited to HTTP. In the latest version, it can transfer binary data and not just JSON-based messages. When configuring SignalR either for ASP.NET or ASP.NET Core, you can select the transport protocol and the message protocol. If you don’t make an explicit choice, the transport protocol is chosen automatically and most of the time it turns out to be WebSockets. The message protocol, by contrast, is based on JSON. Let’s take a look at what happens when a client—for example, a Web client—sets up a connection with a server endpoint. The code below starts a connection.

var progressConnection = new signalR.HubConnection("/progressDemo");
progressConnection.start();

If you monitor the Web traffic generated by this code with a tool like Fiddler, you’ll see that two HTTP requests are sent. Figure 1 shows the details of the first request that bootstraps the conversation.

Details of the Initial Starter Request
Figure 1 Details of the Initial Starter Request

The initial request is an HTTP POST targeted at the SignalR route specified in startup class followed by a /negotiate segment. The following code shows how to define a SignalR route:

app.UseSignalR(routes =>
{
  routes.MapHub<ProgressHub>("/progressDemo");
});

Based on the route, the initial call will target the URL: /progressdemo/negotiate.

Note that in preview versions of SignalR the OPTIONS verb was used for the same purpose. The SignalR server endpoint returns a JSON object configured as follows:

{
  "connectionId" : "b6668ac0-1083-499f-870a-2a5376bf5047",
  "availableTransports" : [
    "WebSockets", "ServerSentEvents", "LongPolling"
  ]
}

As you can see, the JSON response contains two things: the unique ID of the just-established connection, and a list of transport protocols available for use. The sample code indicates that WebSockets, ServerSentEvents and LongPolling can be used given the client and server configuration. What happens next depends on the transport actually chosen by the client.

SignalR tends to use WebSockets if possible. If not, it checks ServerSentEvents and after that falls back to LongPolling. If WebSockets can be used, then an HTTP GET request is placed to the same URL as before. This time the request also adds the connection ID as a query string parameter. The GET request is actually a protocol upgrade request. More precisely, a protocol upgrade is a normal HTTP request (a GET but it can also be a POST) with two ad hoc headers. One is the Connection header, which must be set to Upgrade. The other is Upgrade, which must be set to the name of the required protocol—in this case, WebSockets. If the protocol upgrade is accepted, the server returns an HTTP 101 Switching Protocols response (as shown in Figure 1).

Transport Protocols

SignalR can employ various transport protocols. The client can force the connection to take place over a specific protocol, but by default the protocol is automatically determined. To request a specific transport, simply add an extra parameter to the JavaScript code (or client code if a non-Web client is used). Here’s an example:

var progressConnection = new signalR.HubConnection(
  "/progressDemo",
  {transport : signalR.TransportType.WebSocket});

Note that by passing an array instead of a direct name, you can restrict the choice to one of a few specified protocols. Of course, transport protocols differ in a few aspects. Figure 2 lists the main characteristics of each protocol.

Figure 2 Supported Transport Protocols

Protocol Description
WebSockets Based on a one-to-one connection between the client and server. Both client and server write on a shared pipe so that the flow of the data is bidirectional. The protocol cannot be used everywhere and is limited by the browser and server being used for the connection. On the client, a modern browser is required. The latest versions of all common browsers usually work, with the exception of Internet Explorer prior to version 10. On the server side, when using IIS or HttpSysServer, Windows 8 or newer is required on the client.
ServerSentEvents Based on EventSource object active on the server. The object represents a pipe connecting the server to the client. Once the connection is established, the server can continuously send events while the client can communicate only via plain AJAX calls. The protocol dates back to the early days of Netscape and isn’t supported on the Internet Explorer or Edge browsers.
LongPolling The protocol works by opening a connection with the server to use for future responses. The connection remains pending until a response is sent or the request times out. In any case, once the connection is closed, the client immediately re-establishes it so that polling is continuous but traffic is limited to what’s strictly necessary. This protocol works with all versions of all browsers and is considered a fallback solution.

If the communication between the Web client and the server is cross-domain, then the server must be CORS-enabled. In this case, you can use any available protocol (note that JSONP isn’t supported in ASP.NET Core SignalR):

public void ConfigureServices(IServiceCollection services)
{
  services.AddCors();
}

The browser adds the Origin header to the AJAX and WebSockets calls of ASP.NET Core SignalR. In addition, you need to have the CORS middleware configured in your SignalR application to ensure that the browser allows the requests.

Message Protocols

SignalR in ASP.NET always serialized exchanged messages using the text-based JSON format. The newest ASP.NET Core SignalR adds a binary message protocol based on the MessagePack format. This binary serialization format lets you exchange data in a language-agnostic way, much the way JSON does.

MessagePack is both more compact and faster to transfer than JSON, and features packet sizes that are even more compact than Binary JSON (BSON)—the MongoDB-optimized flavor of JSON. With MessagePack, both small integers and NULL values consume just one byte of data. By comparison, JSON Nulls consumes 4 bytes. For more information about the protocol, check out msgpack.org.

To use MessagePack from an ASP.NET Core SignalR Web client, you just add one more parameter to the JavaScript code that sets up the connection, as shown here:

var protocol = new signalR.protocols.msgpack.MessagePackHubProtocol();
var progressConnection = new signalR.HubConnection(
  "/progressDemo",
  {
    transport : signalR.TransportType.WebSocket,
    protocol : protocol
  }
);

Note that in the case of a Web client willing to use MessagePack, you also must include a separate JavaScript file (signalr-msgpackprotocol.min.js) that you find in the @aspnet/signalr NPM package. MessagePack also requires the ASP.NET Core server to be configured, like so:

public void ConfigureServices(IServiceCollection services)
{
  // SignalR already configured. Just add this:
  services.AddMessagePackProtocol();
}

If you use a non-Web client, all you need to do to enable MessagePack is an extra call in the client application. Specifically, you place a call to the extension method WithMessagePackProtocol defined on the class HubConnectionBuilder.

Non-Web Clients

The most interesting aspect of the .NET Standard specification is that you can consume the same library from within a variety of client applications as long as API compatibility exists. The ASP.NET Core SignalR client library, being based on the .NET Standard 2.0, can be used from within any client applications compiled for a variety of compatible platforms, including the Microsoft .NET Framework 4.6.1 and newer. This means that, say, a Windows Forms application compiled against versions of the .NET Framework newer than 4.6 can happily consume the services of an ASP.NET Core SignalR hub.

With that in mind, let’s see how to launch the lengthy task discussed in last month’s column and monitor it from within a new Windows Forms application. To get started, after creating the application skeleton, reference the NuGet package named Microsoft.AspNetCore.SignalR.Client and all of its dependencies, like so:

private static HubConnection _connection;
private async void Form1_Load(object sender, EventArgs e)
{
  _connection = new HubConnectionBuilder()
    .WithUrl("https://localhost:60000/progressdemo")
    .Build();
  await _connection.StartAsync();
}

The code runs when the form loads up and establishes the connection with the specified SignalR endpoint. If you intend for exchanged data to be serialized using the MessagePack protocol, add one more line to the configuration code of the connection builder object, as follows:

private async void Form1_Load(object sender, EventArgs e)
{
  _connection = new HubConnectionBuilder()
    .WithUrl("https://localhost:60000/progressdemo")
    .WithMessagePackProtocol()
    .Build();
  await _connection.StartAsync();
}

The connection object you receive must be further configured with the client-side handlers responsible for refreshing the UI once notifications from the ASP.NET Core SignalR endpoint come back. Here’s that code:

_connection.On<int>("updateProgressBar", (perc) =>
{
  this.Invoke(
    (Action) (() => label1.Text = String.Format("{0}%", perc))
});

In the sample code, when the updateProgressBar notification is received, a text label is updated with the received value representing the percentage of work currently done. You can have as many handlers as you need and the server-side SignalR counterpart exposes. Figure 3 shows the full rewrite of the last month column’s SignalR back end, as a Windows Forms client would consume it.

Figure 3 The Rewritten SignalR Back End

_connection.On("initProgressBar", () =>
{
  // Run on the UI thread
  Invoke((Action)(() => label1.Text = "0%"));
});
_connection.On<int>("updateProgressBar", (perc) =>
{
  // Run on the UI thread
  Invoke((Action) (() => label1.Text = String.Format("{0}%", perc)));
});
_connection.On("clearProgressBar", () =>
{
  // Run on the UI thread
  Invoke((Action)(() =>
  {
    label1.Text = "100%";
    button1.Enabled = true;
  }));
});
await _connection.StartAsync();

There are a couple of glitches you should be aware of when writing non-Web SignalR clients. First, the client handlers must be fully configured when the connection starts. Specifically, this means that the call to StartAsync should occur after all the due calls to the method On<T> have been made.

Second, keep in mind that you don’t want to run the server-side—and possibly lengthy—operation that SignalR will notify on the same Windows UI thread. That would make the client application unresponsive. To work around the issue, you have to launch the server-side operation on another thread, shown here:

private void button1_Click(object sender, EventArgs e)
{
  Task.Run(() =>
  {
    var client = new WebClient();
    client.UploadString("https://localhost:60000/task/lengthy);
  });
}

Subsequently, when the server-side SignalR hub notifies back, any due changes must be conveyed to the main UI thread before they can take place. You achieve this by using the Invoke method in the client handler. The Invoke method gets a delegate and runs it on the UI thread, as shown here:

Invoke((Action)((perc) =>
  {
    label1.Text = String.Format("{0}%", perc);
  }));

Figure 4 shows the sample Windows Forms application in action with a label progressively updated as the server-side operation makes any progress.

The Windows Forms Client Application Updated by an ASP.NET Core SignalR Hub
Figure 4 The Windows Forms Client Application Updated by an ASP.NET Core SignalR Hub

The Windows Forms application can receive notifications through any of the supported approaches: broadcast, direct connection, groups, single user and streaming. I’ll have more to say about this in a future column.

Wrapping Up

ASP.NET Core SignalR supports the same transport protocols as the previous ASP.NET version, including WebSockets, ServerSent­Events and LongPolling. Furthermore, it supports a binary message protocol in addition to the canonical JSON format.

Like its predecessor, ASP.NET Core SignalR can be called from a variety of different clients—including old-fashioned Windows Forms applications. The key to achieving broad compatibility is support for the .NET Standard 2.0 specification. As we’ve seen in the article, if the client isn’t a .NET Core application, it must be compiled to a version of the .NET Framework that’s compatible with the latest standard. The minimum version required is .NET Framework 4.6.1.

Be sure to check out the source code for this article, which can be found at bit.ly/2FxCKTs.


Dino Esposito has authored more than 20 books and 1,000 articles in his 25-year career. Author of “The Sabbatical Break,” a theatrical-style show, Esposito is busy writing software for a greener world as the digital strategist at BaxEnergy. Follow him on Twitter: @despos.

Thanks to the following Microsoft expert for reviewing this article: Andrew Stanton-Nurse