Code for this article could be found here.

Agenda

I’d like to note that the goal of this article is to show one of the possible ways of calling ASP.NET Core SignalR clients outside of Hub class. The main lesson here is that you don’t want to reference Hub instance from your app’s code - Hubs are transient. The same restriction also applies to old SignalR version - see here.

Domain

The code below implements a use-case of sending arbitrary notifications to all clients connected to a SignalR backend.

Two of our basic interfaces will be

public interface INotificationSource
{
    IEnumerable<NotificationMessage> GetNotifications();
}

and

public interface INotificationTransport
{
    Task Notify(IEnumerable<NotificationMessage> messages);
}

where INotificationTransport is an abstraction for a layer that delivers messages to clients - SignalR in our case.

To connect these two together, we need a third part that will periodically fetch new notifications and send them down the pipe. Something like this:

public class NotificationConnector
{
    private readonly INotificationSource source;
    private readonly INotificationTransport transport;

    public NotificationConnector(
        INotificationSource source, INotificationTransport transport)
    {
        this.source = source;
        this.transport = transport;
    }

    public Task Start(CancellationToken cancellationToken)
    {
        return Task.Run(() => RunLoop(cancellationToken));
    }

    private async Task RunLoop(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            IEnumerable<NotificationMessage> messages = source.GetNotifications();
            if (messages.Any())
            {
                await transport.Notify(messages);
            }

            await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
        }
    }
}

Enter SignalR

Hub

public class NotificationHub : Hub
{
}

INotificationTransport implementation

public class SignalrNotificationTransport : INotificationTransport
{
    private readonly IHubContext<NotificationHub> hubContext;

    public SignalrNotificationTransport(IHubContext<NotificationHub> hubContext)
    {
        this.hubContext = hubContext;
    }

    public Task Notify(IEnumerable<NotificationMessage> messages)
    {
        string payload = JsonConvert.SerializeObject(messages);
        return hubContext.Clients.All.SendAsync("Notify", payload);
    }
}

Note that IHubContext instance is injected into a constructor. Again - you don’t want to keep a reference to NotificationHub in your code, because it has transient lifetime and you won’t be able to change it. Instead, you should work with IHubContext whenever you need to interact with SignalR outside of hubs.

Wiring things up

To connect things together

  • register your dependencies in DI
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddSingleton<INotificationSource, RandomNotificationSource>();
      services.AddSingleton<INotificationTransport, SignalrNotificationTransport>();
      services.AddSingleton<NotificationConnector>();
    
      services.AddSignalR();
    }
    
  • call Start() on NotificationConnector instance

    There are different options here - you can use background service for example, but I chose easier and more dirty way to do this.

    public void Configure(
      IApplicationBuilder app, IWebHostEnvironment env, NotificationConnector connector)
    {
      if (env.IsDevelopment())
      {
          app.UseDeveloperExceptionPage();
      }
    
      app.UseRouting();
    
      app.UseEndpoints(endpoints =>
      {
          endpoints.MapHub<NotificationHub>("/notificationHub");
      });
    
      connector.Start(default(CancellationToken));
    }