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.

To download the source code for this article, you can visit our GitHub repository.

Let’s begin by pointing out some important information about the SignalR Hub and the IHubContext<T> interface.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!