Categories
ASP.NET Core

SignalR Server Broadcasting with ASP.NET Core 3.1

I recently had the challenge of working out how to accomplish SignalR server broadcasting to push real time data for a dashboard. The .NET Core 3.1 docs don’t explain this clearly. The example code below and break down of the principles at play in this article will help get you started with streaming updates from the server using SignalR.

Table of Contents

  1. Hosted Services (Background Services)
  2. SignalR Hub
  3. Connecting to The SignalR Hub
  4. Conclusion

Full code available at: https://github.com/JakeDixon/server-broadcast-signalr-dotnetcore/tree/master

Hosted Services (Background Services)

So one of the key challenges here is how to push the data down to the client using SignalR. The approach I decided upon was a background service (new in ASP.NET Core 2.2 upwards). These are registered through the services.AddHostedService<>() method by implementing (or inheriting from a class that implements) the IHostedService interface.

Hosted services are started and stopped with the website, and thus need registering in the ConfigureServices() method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddSignalR();
    services.AddSingleton<WeatherBackgroundService>();
    services.AddHostedService<BackgroundServiceStarter<WeatherBackgroundService>>();
}

The first two lines should be familiar if you’ve worked with ASP.NET Core.

services.AddControllersWithViews();
services.AddSignalR();

Configures support for MVC and SignalR.

services.AddSingleton<WeatherBackgroundService>();

Registers our WeatherBackgroundService with the dependency injector as a singleton to ensure only one of the class exists.

services.AddHostedService<BackgroundServiceStarter<WeatherBackgroundService>>();

Uses the generic class BackgroundServiceStarter to get the WeatherBackgroundService via dependency injection and register the hosted service. There’s nothing special in the BackgroundServiceStarter as you can see below…

using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace signalr_server_broadcast.BackgroundServices
{
    public class BackgroundServiceStarter<T> : IHostedService where T : IHostedService
    {
        readonly T backgroundService;

        public BackgroundServiceStarter(T backgroundService)
        {
            this.backgroundService = backgroundService;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            return backgroundService.StartAsync(cancellationToken);
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return backgroundService.StopAsync(cancellationToken);
        }
    }
}

If the BackgroundServiceStarter doesn’t do anything special, why not register the WeatherBackgroundService directly in the services.AddHostedService<>();?

The answer comes down to dependency injection. If registered directly to the services.AddHostedService<>(); then you can only inject the service through the IHostedService interface, which will return all services registered by that interface and not the specific service needed.

The WeatherBackgroundService class inherits from the BackgroundService class provided by .NET Core and overrides the ExecuteAsync method to create a loop that runs continuously until the CancellationToken is cancelled.

In the loop the WeatherBackgroundService class utilises the System.Reactive.Subjects.Subject to notify all subscribers of the next update. In the example this is just a randomly generated temperature, with a hard coded location of London and the current DateTime.

using Microsoft.Extensions.Hosting;
using signalr_server_broadcast.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;

namespace signalr_server_broadcast.BackgroundServices
{
    public class WeatherBackgroundService : BackgroundService
    {
        private readonly Subject<WeatherModel> _subject = new Subject<WeatherModel>();
        private readonly Random _random = new Random();

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _subject.OnNext(new WeatherModel { Date = DateTime.Now, Location = "London", Temperature = _random.Next(-10, 40) });

                await Task.Delay(1000);
            }
        }

        public IObservable<WeatherModel> StreamWeather()
        {
            return _subject;
        }
    }
}

Visit Background Tasks with ASP.NET Core 3.1 for more information on background services in ASP.NET Core 3.1

SignalR Hub

Now that the service is starts and stops with the website, we need to provide the SignalR Hub for the front end to connect to.

Create a Hubs folder in the solution.

Hubs folder with WeatherHub class inside.

Then create a WeatherHub class inside that folder.

using Microsoft.AspNetCore.SignalR;
using signalr_server_broadcast.BackgroundServices;
using signalr_server_broadcast.Extensions;
using signalr_server_broadcast.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Channels;
using System.Threading.Tasks;

namespace signalr_server_broadcast.Hubs
{
    public class WeatherHub : Hub
    {
        private readonly WeatherBackgroundService _weatherBackgroundService;
        public WeatherHub(WeatherBackgroundService weatherBackgroundService)
        {
            _weatherBackgroundService = weatherBackgroundService;
        }

        public ChannelReader<WeatherModel> StreamWeather()
        {
            return _weatherBackgroundService.StreamWeather().AsChannelReader(10);
        }
    }
}

The WeatherHub class inherits the SignalR Hub class and accepts in the constructor the WeatherBackgroundService storing it to a private field.

private readonly WeatherBackgroundService _weatherBackgroundService;
public WeatherHub(WeatherBackgroundService weatherBackgroundService)
    {
        _weatherBackgroundService = weatherBackgroundService;
    }

The WeatherHub class exposes one endpoint, SteamWeather(), which produces a stream that the SingalR client can read. There is an IObservableExtensions class that implements the AsChannelReader() extension method, as such it’s worth checking that class out but we’re not going to cover it in this article.

Lastly we need to register the endpoint in the startup.cs. The following code tells the server to route all requests to /hubs/weather to the WeatherHub.

app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<WeatherHub>("/hubs/weather");
    // other endpoints
});

Connecting To The SignalR Hub

We’re not going to cover installing the SignalR.js files because the documentation covers this clearly. https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&tabs=visual-studio

Once installed and loaded in the page add the following to your site.js file.

let connection = new signalR.HubConnectionBuilder().withUrl('/hubs/weather').build();
connection.start().then(() => connection.stream('StreamWeather').subscribe({
    next: (weather) => {
        document.querySelector(".location").innerHTML = weather.location;
        document.querySelector(".temperature").innerHTML = weather.temperature;
        document.querySelector(".date").innerHTML = weather.date;
    },
    error: (err) => console.error(err),
    complete: () => { }
})).catch((err) => console.error(err));

First we define a connection to our endpoint using the route we configured server side.
let connection = new signalR.HubConnectionBuilder().withUrl('/hubs/weather').build();

Next we start the connection and subscribe to the stream defining what to do when the next weather update is pushed to client. In the example we update the place holders with the weather.location, weather.temperature and the weather.date.

There is also callbacks for on completion and error, however for this example we aren’t going to use them.

Conclusion

Server Broadcasting with SignalR
Screenshot of example working

We’ve covered how to implement the background services and inject them into a Hub (or controller) by setting them as a singleton & injecting them into a generic service starter class. We’ve also covered how to expose the background service through a SignalR Hub and connect to it via the SignalR client.

6 replies on “SignalR Server Broadcasting with ASP.NET Core 3.1”

Hi MG,

The concept behind background services is that they start and stop with the website in IIS, (not sure about other web service implementations) so to restart the service you would restart the website.

I would recommend trying to catch the errors to keep the service alive and implement retry logic, or separating the service from the website into a windows service so it can be restarted without the website.

Best regards,
Jake

Very useful post, thanks. One thing I’m unclear about is how the ChannelReader works behind the scenes. Hubs are supposed to be short-lived, right? So what happens with the ChannelReader when the WeatherHub is disposed? How are updates sent through there actually sent out?

Hello,

So the key thing here is that there are two threads on the go, the background service and the actual thread processing the request. The background service is constantly running and generating random weather data. The hub can be viewed as a normal endpoint that return a stream of information across multiple packets with the added advantage that it also supports a WebSocket being opened on it.

When a request is made to the hub, it binds to the observable list and returns it as a ChannelReader (which is a read only attachment to the observable list). When the hub is done with the channel reader it will be GC’ed because there will not be any references to it.

In relation to hubs being short lived, do you remember where you saw that? To the best of my knowledge the entire point of hubs is that they can be long lasting since it opens a websocket between the client and server.

To hopefully cement this question with a concrete example:

  1. The client attaches to the hub
  2. The background service posts a new temperature
  3. The hub gets notified about the new item on the observable list
  4. The hub reads the new item and pushes it down through the channel reader
  5. The client receives the new item

Best regards,
Jake

Leave a Reply

Your email address will not be published. Required fields are marked *