In this article, we will look at how to call a SignalR Hub from an ASP.NET Core controller.
If you are unfamiliar with setting up SignalR in your server and/or client application, check out our article for more information.
Let’s begin by pointing out some important information about the SignalR Hub and the IHubContext<T> interface.
Overview of SignalR Hub Class and IHubContext Interface
The SignalR Hub
serves as the base component for SignalR-based interactions. The methods we declare within the Hub are specifically for our connected clients to invoke. They should not be accessed internally from our server because their intended usage is for client-side invocation.
However, there are scenarios where we need to push notifications from the server to connected clients without a prior client invocation. An example is when we have a restaurant’s server that sends out a daily menu to subscribed clients.
In such cases, we require a hub method to access connected and subscribed clients. We need an alternative approach to call SignalR hub methods from within the server. This is where the SignalR IHubContext<THub>
becomes crucial.
Call SignalR Hub from an ASP.NET Core Controller
IHubContext<THub>
is a service that allows us to call Hub methods from outside the Hub. It has a property called Clients
of type Microsoft.AspNetCore.SignalR.IHubClients
through which we can invoke methods on connected clients.
We can make use of IHubContext<T> by simply injecting it into our controller. Let’s create a RandomizerController and inject the hub context into it:
[Route("api/[controller]")] [ApiController] public class RandomizerController : ControllerBase { private readonly IHubContext<RandomizerHub> _hub; public RandomizerController(IHubContext<RandomizerHub> hub) { _hub = hub; } }
Here, we hooked IHubContext<T>
with a SignalR Hub named RandomizerHub
. Then we injected IHubContext<RandomizerHub>
into our controller named RandomizerController
. With this setup, our controller methods have access to all the clients connected to RandomizerHub
:
public class RandomizerHub : Hub { }
Here we are intentionally leaving the RandomizerHub
empty because our example’s focus is solely on the transmission of information from the server to connected clients. Therefore, we don’t need to define methods directly within the hub.
We can use the _hub.Clients
method in the controller to interact with these clients just as we would in a SignalR Hub:
[HttpGet("SendRandomNumber")] public ActionResult<int> SendRandomNumber() { var randomValue = Random.Shared.Next(1, 51) * 2; _hub.Clients.All .SendAsync("SendClientRandomEvenNumber", randomValue); return Ok(randomValue); }
In the snippet, we define a controller method named SendRandomNumber
. This method utilizes _hub.Clients.All.SendAsync
to broadcast random even numbers, ranging from 2 to 100, to all connected clients subscribed to the SendClientRandomEvenNumber
event. Note here that while SendAsync
is an asynchronous method we are not awaiting this call to complete because the API response is not dependent on its completion. The API will return a 200 OK response to the client. Later, via the signalR connection, the generated random even number will be broadcast to all the subscribed clients.
Simulate a Recurring Server-Client Interaction With SignalR Hub and ASP.NET Core Controller
In a production environment, we will likely configure our server with a cron job that periodically triggers our controller method to send messages to our connected clients.
To replicate this recurring communication pattern in our example, we can use the Timer class described in our previous article:
public class TimerManager { private Timer? _timer; private Action? _action; public DateTime TimerStarted { get; set; } public bool IsTimerStarted { get; set; } public void PrepareTimer(Action action) { _action = action; _timer = new Timer(Execute, null, 1000, 4000); TimerStarted = DateTime.Now; IsTimerStarted = true; } public void Execute(object? stateInfo) { if (_action != null) { _action(); } if ((DateTime.Now - TimerStarted).TotalSeconds > 60) { IsTimerStarted = false; if (_timer != null) { _timer.Dispose(); } } } }
Our TimerManager
class is responsible for managing the timer functionality. Aside from the methods it uses to prepare and execute the timer, it also contains properties that can help us track the timer’s status.
We inject TimerManager
into our controller and use it to initiate calls to the client at specified intervals:
public class RandomizerController : ControllerBase { private readonly IHubContext<RandomizerHub> _hub; private readonly TimerManager _timer; public RandomizerWithNonGenericHubController(IHubContext<RandomizerHub> hub, TimerManager timer) { _hub = hub; _timer = timer; } [HttpGet("SendRandomNumber")] public ActionResult<int> SendRandomNumber() { var randomValue = Random.Shared.Next(1, 51) * 2; if (!_timer.IsTimerStarted) { _timer.PrepareTimer(() => _hub.Clients.All .SendAsync("SendClientRandomEvenNumber", randomValue)); } return Ok(randomValue); } }
For a smooth flow of information from our sample server to the connected clients, our connected clients must be actively listening for a broadcast specifically named “SendClientRandomEvenNumber”.
While using _hub.Clients.All.SendAsync
is effective, it introduces a potential issue because it relies on a hardcoded string to specify the client method to be called. Any typographical or semantic error in declaring the method that the clients are listening to could disrupt the entire process.
A typical example is when we configure our client to listen for “SendClientRandomEvenNumber,” but inadvertently, we declare “SendClientRandomEvenNumbr” on the server.
So, let’s see how we can prevent this mistake.
Using a Strongly Typed IHubContext To Call SignalR Hub Method from Controller in ASP.NET Core
A more secure approach to interacting with IHubContext<THub>
is by utilizing a strongly typed HubContext
.
We can achieve this in three easy steps. First, we declare an interface that enforces the correct method signature we require in our hub:
public interface IRandomizerClient { void SendClientRandomEvenNumber(int number); }
We define an interface IRandomizerClient
, comprising a single method named SendClientRandomEvenNumber
. This method accepts an int
parameter and has a void
return type.
Then we make our Hub class implement this interface:
public class RandomizerHub : Hub<IRandomizerClient> { }
Finally, in our controller’s constructor, we substitute IHubContext<THub>
with IHubContext<THub, T>
, where THub
is our RandomizerHub
, and T
is the interface IRandomizerClient
:
private readonly IHubContext<RandomizerHub, IRandomizerClient> _hub; private readonly TimerManager _timer; public RandomizerController(IHubContext<RandomizerHub, IRandomizerClient> hub, TimerManager timer) { _hub = hub; _timer = timer; }
With this update, we can access the hub’s methods using strongly typed method names instead of hardcoded strings.
Subsequently, we can update SendRandomNumber
action method to use SendClientRandomEvenNumber
method declared in the interface:
[HttpGet("SendRandomNumber")] public ActionResult<int> SendRandomNumber() { var randomValue = Random.Shared.Next(1, 51) * 2; if (!_timer.IsTimerStarted) { _timer.PrepareTimer(() => _hub.Clients.All .SendClientRandomEvenNumber(randomValue)); } return Ok(randomValue); }
Conclusion
In this article, we discussed how to call SignalR Hub from an ASP.NET Core controller. We started this article by discussing the SignalR Hub and how our connected clients interact with it. Then we entered into the meat of the conversation where we saw how to correctly inject IHubContext
into our controller and leverage it for client interactions. As a final step, we looked at how we can address the risk of human error when we declare methods that our connected clients are to listen to.