Search code examples
asp.net-core.net-coresignalrsignalr-hubasp.net-core-signalr

How to inject strongly typed signalR hub in a class [ASP CORE 2.2]


I want to inject my strongly typed hub in a service, but I don't like certain thing in the example shown by Microsoft - https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext?view=aspnetcore-2.2 (Inject a strongly-typed HubContext)

public class ChatController : Controller
{
    public IHubContext<ChatHub, IChatClient> _strongChatHubContext { get; }

    public ChatController(IHubContext<ChatHub, IChatClient> chatHubContext)
    {
        _strongChatHubContext = chatHubContext;
    }

    public async Task SendMessage(string message)
    {
        await _strongChatHubContext.Clients.All.ReceiveMessage(message);
    }
}

In this example ChatHub is coupled to ChatController.

So I want to inject the hub itself defined with generic interface parameter and no concrete implementation of it will be defined in my service. This is sample code

public interface IReportProcessingClient
{
    Task SendReportInfo(ReportProgressModel report);
}
public class ReportProcessingHub : Hub<IReportProcessingClient>
{
    public async Task SendMessage(ReportProgressModel report)
    {
        await Clients.All.SendReportInfo(report);
    }
}

 public class ReportInfoHostedService : IHostedService, IDisposable
 {
     private readonly Hub<IReportProcessingClient> _hub;
     private readonly IReportGenerationProgressService _reportService;

     public ReportInfoHostedService(Hub<IReportProcessingClient> hub, IReportGenerationProgressService reportService)
     {
         _hub = hub;
         _reportService = reportService;
     }

     public Task StartAsync(CancellationToken cancellationToken)
     {
         _reportService.SubscribeForChange(async x =>
         {
             await _hub.Clients.All.SendReportInfo(x);
         });

         return Task.CompletedTask;
     }

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

     public void Dispose()
     {
     }
 }

This approach obviously will need additional registration of the hub in Startup.cs as it is not called by the context api provided by Microsoft.

services.AddSingleton<Hub<IReportProcessingClient>, ReportProcessingHub>();
app.UseSignalR(route => {
      route.MapHub<ReportProcessingHub>("/reportProcessingHub");
});

All done and working until the hub is trying to send messages to Clients. Then I get the exception

_hub.Clients.All threw an exception of System.NullReferenceException: 'Object reference not set to an instance of an object.'

So to summerize:

1. Is this the right way to inject strongly typed hubs and what am I doing wrong(e.g. wrong registration of the hub in services, wrong usage of app.UseSingleR)?

2. If not, what is the correct way?

NOTE: I know there is a lot easier way injecting IHubContext<Hub<IReportProcessingClient>>, but this is not a solution for me, because I have to call the hub method name passed as string parameter.


Solution

  • I want to ... and no concrete implementation of it will be defined in my service

    1. If you don't want to expose a concrete hub implementation, you should at least expose a base class or interface. However, since a hub should inherit from the Hub class, we can't use an interface here. So let's create a public base hub ReportProcessingHubBase as well as an internal concrete ReportProcessingHub:

      public abstract class ReportProcessingHubBase : Hub<IReportProcessingClient>
      {
          public abstract Task SendMessage(ReportProgressModel report);
      }
      
      // the concrete hub will NOT be exposed
      internal class ReportProcessingHub : ReportProcessingHubBase
      {
          public override async Task SendMessage(ReportProgressModel report)
          {
              await Clients.All.SendReportInfo(report);
          }
      }
      
    2. Make sure you've registered the two related service:

      services.AddScoped<ReportProcessingHubBase, ReportProcessingHub>();
      services.AddHostedService<ReportInfoHostedService>();
      
    3. Make sure you're mapping the Base Hub (MOST IMPORTANT!):
       endpoints.MapHub<ReportProcessingHubBase>("/report");
      
    4. Finally, you can get the base hub by injecting IHubContext<ReportProcessingHubBase,IReportProcessingClient> :

      public class ReportInfoHostedService : IHostedService, IDisposable
      {
          private readonly IHubContext<ReportProcessingHubBase,IReportProcessingClient> _hub;
      
          public ReportInfoHostedService(IHubContext<ReportProcessingHubBase,IReportProcessingClient> hub)
          {
              _hub = hub;
          }
      
          ...
       }
      

      Now, you can invoking the hub method in a strongly-typed way.