Host ASP.NET Core SignalR in background services

By Brady Gaster

This article provides guidance for:

  • Hosting SignalR Hubs using a background worker process hosted with ASP.NET Core.
  • Sending messages to connected clients from within a .NET Core BackgroundService.

View or download sample code (how to download)

Wire up SignalR during startup

Hosting ASP.NET Core SignalR Hubs in the context of a background worker process is identical to hosting a Hub in an ASP.NET Core web app. In the Startup.ConfigureServices method, calling services.AddSignalR adds the required services to the ASP.NET Core Dependency Injection (DI) layer to support SignalR. In Startup.Configure, the MapHub method is called in the UseEndpoints callback to wire up the Hub endpoint(s) in the ASP.NET Core request pipeline.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSignalR();
        services.AddHostedService<Worker>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<ClockHub>("/hubs/clock");
        });
    }
}

Hosting ASP.NET Core SignalR Hubs in the context of a background worker process is identical to hosting a Hub in an ASP.NET Core web app. In the Startup.ConfigureServices method, calling services.AddSignalR adds the required services to the ASP.NET Core Dependency Injection (DI) layer to support SignalR. In Startup.Configure, the UseSignalR method is called to wire up the Hub endpoint(s) in the ASP.NET Core request pipeline.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSignalR();
        services.AddHostedService<Worker>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseSignalR((routes) =>
        {
            routes.MapHub<ClockHub>("/hubs/clock");
        });
    }
}

In the preceding example, the ClockHub class implements the Hub<T> class to create a strongly typed Hub. The ClockHub has been configured in the Startup class to respond to requests at the endpoint /hubs/clock.

For more information on strongly typed Hubs, see Use hubs in SignalR for ASP.NET Core.

Note

This functionality isn't limited to the Hub<T> class. Any class that inherits from Hub, such as DynamicHub, will also work.

public class ClockHub : Hub<IClock>
{
    public async Task SendTimeToClients(DateTime dateTime)
    {
        await Clients.All.ShowTime(dateTime);
    }
}

The interface used by the strongly typed ClockHub is the IClock interface.

public interface IClock
{
    Task ShowTime(DateTime currentTime);
}

Call a SignalR Hub from a background service

During startup, the Worker class, a BackgroundService, is wired up using AddHostedService.

services.AddHostedService<Worker>();

Since SignalR is also wired up during the Startup phase, in which each Hub is attached to an individual endpoint in ASP.NET Core's HTTP request pipeline, each Hub is represented by an IHubContext<T> on the server. Using ASP.NET Core's DI features, other classes instantiated by the hosting layer, like BackgroundService classes, MVC Controller classes, or Razor page models, can get references to server-side Hubs by accepting instances of IHubContext<ClockHub, IClock> during construction.

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IHubContext<ClockHub, IClock> _clockHub;

    public Worker(ILogger<Worker> logger, IHubContext<ClockHub, IClock> clockHub)
    {
        _logger = logger;
        _clockHub = clockHub;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {Time}", DateTime.Now);
            await _clockHub.Clients.All.ShowTime(DateTime.Now);
            await Task.Delay(1000);
        }
    }
}

As the ExecuteAsync method is called iteratively in the background service, the server's current date and time are sent to the connected clients using the ClockHub.

React to SignalR events with background services

Like a Single Page App using the JavaScript client for SignalR or a .NET desktop app can do using the using the ASP.NET Core SignalR .NET Client, a BackgroundService or IHostedService implementation can also be used to connect to SignalR Hubs and respond to events.

The ClockHubClient class implements both the IClock interface and the IHostedService interface. This way it can be wired up during Startup to run continuously and respond to Hub events from the server.

public partial class ClockHubClient : IClock, IHostedService
{
}

During initialization, the ClockHubClient creates an instance of a HubConnection and wires up the IClock.ShowTime method as the handler for the Hub's ShowTime event.

private readonly ILogger<ClockHubClient> _logger;
private HubConnection _connection;

public ClockHubClient(ILogger<ClockHubClient> logger)
{
    _logger = logger;
    
    _connection = new HubConnectionBuilder()
        .WithUrl(Strings.HubUrl)
        .Build();

    _connection.On<DateTime>(Strings.Events.TimeSent, 
        dateTime => _ = ShowTime(dateTime));
}

public Task ShowTime(DateTime currentTime)
{
    _logger.LogInformation("{CurrentTime}", currentTime.ToShortTimeString());

    return Task.CompletedTask;
}

In the IHostedService.StartAsync implementation, the HubConnection is started asynchronously.

public async Task StartAsync(CancellationToken cancellationToken)
{
    // Loop is here to wait until the server is running
    while (true)
    {
        try
        {
            await _connection.StartAsync(cancellationToken);

            break;
        }
        catch
        {
            await Task.Delay(1000);
        }
    }
}

During the IHostedService.StopAsync method, the HubConnection is disposed of asynchronously.

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

Additional resources