Streaming In ASP.NET Core SignalR

In this post, we’ll see how to stream the data in ASP.NET Core SignalR. With ASP.NET Core 2.1 released, SignalR now supports streaming content.

What is a Stream?

"Streaming or media streaming is a technique for transferring data so that it can be processed as a steady and continuous stream." – webopedia.com

When to stream?

In scenarios where the data will have some latency from the server and we don’t want to wait for the content to arrive. For this scenario, we can use data streaming.

This can also be useful when we don’t want to get all the data at a time as this might be time-consuming. So, we’ll send the data in chunks/fragments from the server to the client.

Now, we’ll see how to set up the ASP.NET Core SignalR in Visual Studio.

Creating ASP.NET Core Application

It’s always the same thing: Go over to File >> New Project and give it a name.

Streaming In ASP.NET Core SignalR

And now select Web Application from the templates and your framework as ASP.NET Core 2.1,

Streaming In ASP.NET Core SignalR

Once you’re done with it you should have your project in the solution folder.

We’ll start writing our Hubs now.

Hub setup

Unlike normal signalR methods, the stream methods will be different as they have to stream the content over time when the chunks of data are available.

Create a C# file in the project with the name StreamHub or whatever you like. It is better to add it in a Folder though.

Derive that from Hub class and add a namespace in the file using Microsoft.AspNetCore.SignalR.

Now, create a method in the class with the return type as ChannelReader<T> where T is the type of the value returned. The ChannelReader return type on a method makes a streaming hub method. Here is the code for streaming our data.

  1. public ChannelReader<int> DelayCounter(int delay)  
  2.         {  
  3.             var channel = Channel.CreateUnbounded<int>();  
  4.   
  5.             _ = WriteItems(channel.Writer, 20, delay);  
  6.   
  7.             return channel.Reader;  
  8.         }  
  9.   
  10.         private async Task WriteItems(ChannelWriter<int> writer, int count, int delay)  
  11.         {  
  12.             for (var i = 0; i < count; i++)  
  13.             {  
  14.                 //For every 5 items streamed, add twice the delay  
  15.                 if (i % 5 == 0)  
  16.                     delay = delay * 2;  
  17.   
  18.                 await writer.WriteAsync(i);  
  19.                 await Task.Delay(delay);  
  20.             }  
  21.   
  22.             writer.TryComplete();  
  23.         }  
  • DelayCounter is our streaming method, this takes a delay parameter to specify from the client end.
  • WriteItems is a private method and this returns a Task.
  • The last line in the WriteItems is .TryComplete() on the stream says the stream is completed and is closed to the client.
Configuring SignalR in the project
  1. Head over to the Startup class and locate ConfigureServices method and add the following line at the end (skip this if you can configure it yourself).
    1. services.AddSignalR();  
  1. We also need to add a route for the signalR stream. Now, head over to the Configure method in the Startup class and add the following.
    1. app.UseSignalR(routes =>  
    2.             {  
    3.                 routes.MapHub<StreamHub>("/streamHub");  
    4.             });  
Add SignalR client library

This is to add the signalR js on the client side.

Launch Package Manager Console (PMC) from the Visual Studio and navigate to project folder with the following command.

cd CodeRethinked.SignalRStreaming

Run npm init to create a package.json file

npm init -y

Ignore the warnings. Install the signalR client library.

npm install @aspnet/signalr

The npm install downloads the signalR client library to a subfolder under the node_modules folder.

Copy the signalR from node_modules

Copy the signalr.js file from the <project_folder>\node_modules\@aspnet\signalr\dist\browser to a folder in wwwroot\lib\signalr.

or

Alternatively, you could also make use of the Microsoft Library Manager (libman.json) to restore it for you.

If you don’t have any idea of what libman.json is, check this article on Libman.

So, your Libman for adding downloaded signalR should look like this.

  1. {  
  2.   "version""1.0",  
  3.   "defaultProvider""cdnjs",  
  4.   "libraries": [  
  5.     {  
  6.       "provider""filesystem",  
  7.       "library""node_modules/@aspnet/signalr/dist/browser/signalr.js",  
  8.       "destination""wwwroot/lib/signalr"  
  9.     }  
  10.   ]  
  11. }  

Once you’ve saved libman.json our signalr.js will be available in the SignalR folder in lib.

HTML for streaming

Copy the following HTML into Index.chtml. For the purposes of the article, I’m removing the existing HTML in Index.cshtml and adding the following.

  1. @page  
  2. @model IndexModel  
  3. @{  
  4.     ViewData["Title"] = "Home page";  
  5. }  
  6.   
  7. <div class="container">  
  8.     <div class="row"> </div>  
  9.     <div class="row">  
  10.         <div class="col-6"> </div>  
  11.         <div class="col-6">  
  12.             <input type="button" id="streamButton" value="Start Streaming" />  
  13.         </div>  
  14.     </div>  
  15.     <div class="row">  
  16.         <div class="col-12">  
  17.             <hr />  
  18.         </div>  
  19.     </div>  
  20.     <div class="row">  
  21.         <div class="col-6"> </div>  
  22.         <div class="col-6">  
  23.             <ul id="messagesList"></ul>  
  24.         </div>  
  25.     </div>  
  26. </div>  
  27. <script src="~/lib/signalr/signalr.js"></script>  
  28. <script src="~/js/signalrstream.js"></script>  

Notice we have signalrstream.js at the end. Let’s add the js file to stream the content.

JavaScript setup

Create a new signalrstream.js file in wwwroot\js folder. Add the following code into the js file.

  1. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {  
  2.     return new (P || (P = Promise))(function (resolve, reject) {  
  3.         function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }  
  4.         function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }  
  5.         function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }  
  6.         step((generator = generator.apply(thisArg, _arguments || [])).next());  
  7.     });  
  8. };  
  9.   
  10. var connection = new signalR.HubConnectionBuilder()  
  11.     .withUrl("/streamHub")  
  12.     .build();  
  13. document.getElementById("streamButton").addEventListener("click", (event) => __awaiter(thisvoid 0, void 0, function* () {  
  14.     try {  
  15.         connection.stream("DelayCounter", 500)  
  16.             .subscribe({  
  17.                 next: (item) => {  
  18.                     var li = document.createElement("li");  
  19.                     li.textContent = item;  
  20.                     document.getElementById("messagesList").appendChild(li);  
  21.                 },  
  22.                 complete: () => {  
  23.                     var li = document.createElement("li");  
  24.                     li.textContent = "Stream completed";  
  25.                     document.getElementById("messagesList").appendChild(li);  
  26.                 },  
  27.                 error: (err) => {  
  28.                     var li = document.createElement("li");  
  29.                     li.textContent = err;  
  30.                     document.getElementById("messagesList").appendChild(li);  
  31.                 },  
  32.             });  
  33.     }  
  34.     catch (e) {  
  35.         console.error(e.toString());  
  36.     }  
  37.     event.preventDefault();  
  38. }));  
  39.   
  40. (() => __awaiter(thisvoid 0, void 0, function* () {  
  41.     try {  
  42.         yield connection.start();  
  43.     }  
  44.     catch (e) {  
  45.         console.error(e.toString());  
  46.     }  
  47. }))();  

ASP.NET SignalR now uses ES 6 features and not all browsers support ES 6 features. So, in order for it to work in all browsers, it is recommended to use transpilers such as babel.

Unlike traditional signalR, we now have different syntax for creating a connection.

  1. var connection = new signalR.HubConnectionBuilder()  
  2.     .withUrl("/streamHub")  
  3.     .build();  

And for regular signalR connections, we’ll add listeners with .on method but this is streamed so we have a stream method that accepts two arguments.

  • Hub method name: Our hub name is DelayCounter
  • Arguments to the Hub method: In our case arguments is a delay between the streams.

connection.stream will have to use subscribe method to subscribe to events. We’ll wire up for next, complete and error events and display messages in the messagesList element.

  1. connection.stream("DelayCounter", 500)  
  2.     .subscribe({  
  3.         next: (item) => {  
  4.             var li = document.createElement("li");  
  5.             li.textContent = item;  
  6.             document.getElementById("messagesList").appendChild(li);  
  7.         },  
  8.         complete: () => {  
  9.             var li = document.createElement("li");  
  10.             li.textContent = "Stream completed";  
  11.             document.getElementById("messagesList").appendChild(li);  
  12.         },  
  13.         error: (err) => {  
  14.             var li = document.createElement("li");  
  15.             li.textContent = err;  
  16.             document.getElementById("messagesList").appendChild(li);  
  17.         },  
  18. });  

The code before/after the stream connection is related to async and start a connection as soon as we hit the js file.

Here is the output of the stream,

Streaming In ASP.NET Core SignalR

 

See it in action,

Streaming In ASP.NET Core SignalR

I’ve modified the StreamHub class to have the count up to 10 in the above gif image so that it won’t take any longer.

Notice the delay from items 6-10 when streaming. This is because we’ve doubled the amount of delay for every 5 items. This can be thought of as streaming the data only when available. So, the 6th item is streamed when it is available.

So, if you have a large amount of data to be sent to the client, then go for streaming instead of sending the data all at once.

Source code download

In the source code, I’ve removed the npm_modules from the solution to make it lightweight so install the npm modules with the following command and start the solution.

npm install

Takeaway

Streaming the content is not new but it is in signalR now and a great feature. Streaming will keep the user experience pretty cool and also our server won’t have those high bars (peak timings).

Most of the developers know the limitations of SignalR not being able to transmit a huge amount of data.

With ASP.NET Core SignalR, streaming the data from the server to client overcomes the problem of transferring all the content at once.

I’d recommend going for streaming content when you think your data is large or if you want some user experience without blocking the client by showing endless spinners.