Introduction
SignalR has become a popular means for communication in rapidly growing .NET Core realm. So I think it would be useful to add data streaming capabilities to SignalR server and client. By data streaming here, I mean scenario when server continuously produces data and pushes them to its subscribers (clients). For readers not familiar with SignalR, I'd recommend to start with Introduction to ASP.NET Core SignalR. This article provides such an infrastructure for SignalR .NET Core server and client and also for JavaScript client.
Hub Related Infrastructure
Server
SignalRSvc
solution contains Infrastructure folder with three projects (components), namely, AsyncAutoResetEventLib
, SignalRBaseHubServerLib
and SignalRBaseHubClientLib
. Component AsyncAutoResetEventLib
taken from here, implements class AsyncAutoResetEvent
that is asynchronous version of an auto-reset event. Class StreamingHub<T>
of component SignalRBaseHubServerLib
extends base framework class Microsoft.AspNetCore.SignalR.Hub
and implements interface ISetEvent
with its method void SetEvent()
and property bool IsValid { get; }
. Code of StreamingHub<T>
is given below.
public class StreamingHub<T> : Hub, ISetEvent
{
protected readonly IStreamingDataProvider<T> _streamingDataProvider;
private readonly AsyncAutoResetEvent _aev = new AsyncAutoResetEvent();
private int _isValid = 0;
protected StreamingHub(StreamingDataProvider<T> streamingDataProvider)
{
IsValid = true;
streamingDataProvider.Add(this);
_streamingDataProvider = streamingDataProvider;
}
public ChannelReader<T> StartStreaming()
{
return Observable.Create<T>(async observer =>
{
while (!Context.ConnectionAborted.IsCancellationRequested)
{
await _aev.WaitAsync();
observer.OnNext(_streamingDataProvider.Current);
}
}).AsChannelReader();
}
public bool IsValid
{
get => Interlocked.Exchange(ref _isValid, _isValid) == 1;
private set => Interlocked.Exchange(ref _isValid, value ? 1 : 0);
}
public void SetEvent()
{
_aev.Set();
}
protected override void Dispose(bool disposing)
{
IsValid = false;
base.Dispose(disposing);
}
}
The main purpose of this class is to add streaming capabilities to a hub. Its constructor gets as an argument reference to an object with base class StreamingDataProvider<T>
where T
is a data type to be transferred. Class StreamingDataProvider<T>
is also implemented in SignalRBaseHubServerLib
component. StreamingDataProvider<T>
supports list of ISetEvent
interfaces that are actually streaming hubs. Its setter Current
calls method SetEvent()
for all valid streaming hubs and "forgets" the hubs already disposed. Call of SetEvent()
method releases streaming mechanism implemented in method StartStreaming()
of type StreamingHub<T>
, and newly assigned instance of T
type is pushed to all streaming subscribers (filtering of the subscriber is not implemented for the sake of simplicity).
The described above hub provides streaming capability for derived hub classes out-of-the-box. Data provider class derived from base class StreamingDataProvider<T>
should periodically call setter Current
to ensure streaming of T
instance to subscribers (clients).
.NET Core Client
Widespread clients of SignalR hubs are .NET Core applications and even more common Web application that use JavaScript. Let's start with the former one. Class HubClient
of SignalRBaseHubClientLib
component provides handy mechanism for streaming subscription. It also may act as a base class for .NET Core hub clients types. Its constructor takes server's URL as an argument and creates instance of Microsoft.AspNetCore.SignalR.Client.HubConnection
class. Then method async Task<bool> SubscribeAsync<T>(Action<T> callback)
is called to subscribe for T
object stream from server. Body of the method is provided below:
public async Task<bool> SubscribeAsync<T>(Action<T> callback)
{
if (Connection == null || _cts.Token.IsCancellationRequested || callback == null)
return false;
try
{
var channel = await Connection.StreamAsChannelAsync<T>("StartStreaming", _cts.Token);
while (await channel.WaitToReadAsync())
while (channel.TryRead(out var t))
{
try
{
callback(t);
}
catch (Exception e)
{
throw new Exception($"Hub \"{Url}\" Subscribe(): callback had failed. ", e);
}
}
return true;
}
catch (OperationCanceledException)
{
return false;
}
}
As an argument method SubscribeAsync<T>()
receives a user provided callback to process newly obtained T
object.
JavaScript Client
Web application SignalR client component code is placed to file comm.js in folder SignalRSvc\wwwroot. It is based on the code snippet published here. To use the code, JavaScript SignalR client package should be installed. To install it with npm
package manager, we run the following commands in console:
npm init -y
npm install @aspnet/signalr
As a result, we will get directory node_modules. Then for convenience, we copy file node_modules\@aspnet\signalr\dist\browser\signalr.js to a new directory SignalRSvc\wwwroot\lib\signalr.
Code in file SignalRSvc\wwwroot\index.js shows how to use communication functions from comm.js. hubConnection
object is created and then the scenario is similar to .NET Core client. Communication related code from file comm.js is listed below:
function createHubConnection(url) {
return new signalR.HubConnectionBuilder()
.withUrl(url)
.configureLogging(signalR.LogLevel.Information)
.build();
}
async function startHubConnection(hubConnection) {
try {
await hubConnection.start();
console.log('Comm: Successfully connected to hub ' + url + ' .');
return true;
}
catch (err) {
console.error('Comm: Error in hub startHubConnection().
Unable to establish connection with hub ' + url + ' . ' + err);
return false;
}
}
async function startStreaming(hubConnection, serverFuncName, callback) {
isOK = false;
try {
await hubConnection.stream(serverFuncName)
.subscribe({
next: item => {
try {
callback(item);
}
catch (err) {
console.error('Comm: Error in hub streaming callback. ' + err);
}
},
complete: () => console.log('Comm: Hub streaming completed.'),
error: err => console.error('Comm: Error in hub streaming subscription. ' + err)
});
console.log('Comm: Hub streaming started.');
isOK = true;
}
catch (err) {
console.error('Comm: Error in hub startStreaming(). ' + err);
}
return isOK;
}
Please note that this code is written with standard ES8 (a.k.a. ECMAScript 2017) of JavaScript and not all browsers support it as of today. I tested it with Google Chrome browser.
Code Sample
Streaming capabilities described above are illustrated with the code sample. The solution consists of Infrastructure folder with its three projects, AsyncAutoResetEventLib
, SignalRBaseHubServerLib
and SignalRBaseHubClientLib
described above, projects ModelLib
supplying data transfer object Dto
, DtoProviderLib
providing class DtoEventProvider : StreamingDataProvider<Dto>
, Web service SignalRSvc
contains hub and controller, and its .NET Core client SignalRClientTest
.
SignalRSvc
provides a hub class TheFirstHub : StreamingHub<Dto>
with method ProcessDto()
that can be called by clients. The service also has AboutController
which method(s) may be called as in ordinary RESTful service. The service may be accessed with HTTP and HTTPS calls. HTTPS mode is achieved by building service with HTTPS conditional compilation symbol, but may be made configurable with minor changes in the code, if required. SignalRSvc
possesses Cross-Origin Resource Sharing (CORS) capabilities which is important for Web application clients. Service can be reached with the following URLs:
| HTTP | HTTPS |
Hub | http://0.0.0.0:15000/hub/the1st | https://0.0.0.0:15001/hub/the1st |
Controller Method | http://0.0.0.0:15000/api/about | https://0.0.0.0:15001/api/about |
.NET Core client application SignalRClientTest
calls method static async void MainAsync()
and goes into a while
-loop waiting for keyboard input. Asynchronous method MainAsync()
instantiates class HubClient
from SignalRBaseHubClientLib
infrastructure project, starts connection to the service, provides handler for the ReceiveMessage()
server hub's push call, calls remotely method ProcessDto()
of the service hub and finally calls method SubscribeAsync<Dto>()
of HubClient
to subscribe for streaming from the service. Handler of received Dto
objects is given as an argument to SubscribeAsync<Dto>()
method. Meanwhile client waits for keyboard input in the loop as it was stated above. If user inserts character other than 'q
' or 'Q
', then hub method ProcessDto()
will be called and the service will notify clients pushing ReceiveMessage()
to all of them.
Dealing with the source code, you may run command files Run_SignalRSvc.cmd and Run_SignalRTestClient.cmd located in the solution directory. The command files build default configuration (currently Debug) and start Web service and its .NET Core client respectively. You will see log messages appeared in the client console. There are two types of messages shown. Most of the messages are produced as a result of streaming. They are Dto
objects with Guid ClientId
and random integer for Data
. Timer handler in class DtoEventProvider
generates the objects. The second type of messages appears on either start of a new client or on pressing any key but 'q
' in the client console. This message depicts name of source client and data received. According to our Web service policy, all messages are sent to all clients. To quit client, press key 'q
'.
Now let's discuss Web application client. Testing the sample I ran it under Windows only using IIS Express Web Server. First, we have to set up an appropriate site in IIS Express configuration file applicationhost.config located in %user%\Documents\IISExpress\config folder. Definition of the site called SignalRClientSite is provided in Read_Me.txt file of the code sample. This definition should be placed into <sites>
tag of applicationhost.config file (please pay attention that value of the site id should be unique). Then we run command file Run_IISExpressTestSite.cmd starting client Web site SignalRClientSite in IIS Express. When the Web site has been started, you test Web application from browser (I used Google Chrome) with URL http://localhost:15015. Activated Web page connects to our hub on http://localhost:15000/hub/the1st and then will print the same messages as .NET Core client does.
To test HTTPS case, you will be required to make the following changes:
- In projects
SignalRSvc
and SignalRClientTest
in their Properties -> Build tab, insert Conditional compilation symbol HTTPS - In file SignalRSvc\wwwroot\index.js in the first line, change value of
const HTTPS
to true
.
We configure our IIS Express to be able start Web application with HTTPS using URL http://localhost:44399 (port 443?? was chosen because most likely it has been already bound to a SSL certificate - this may be checked with the following command line call:
netsh http show sslcert
Since this is the same application, it will call our Web service in the same manner as it was described above.
To run demo, you will be required to run both cmd files from the demo root directory. They start Web service containing hub and its .NET Core client. For the sake of simplicity, usage of Web client is not presented in demo.
The sample was also tested in Linux environment (Ubuntu 18.04). In order to move the sample to Linux, please run files _publish.cmd in both SignalRSvc and SignalRClientTest directories. Folders publish will be generated in both places. Then contents of publish folders for both SignalRSvc and SignalRClientTest should be copied to Linux machine where .NET Core is installed. For Linux environment, I used Oracle VM VirtualBox and MobaXterm application to copy files from Windows and run them in Linux.
Conclusions
This work presents handy infrastructure components for SignalR communication in .NET Core, particularly for data streaming from server to clients. Appropriate components may be seamlessly incorporated to .NET Core server and client applications. JavaScript client component is also provided to be used in Web application clients.