C# – SignalR: How to truly call a hub’s method from the server / C#

csignalr

I'm trying to improve my application which will require calling a hub from C# instead of javascript. The current workflow for adding a task in my app is:

  • make an API call to add the data to the database
  • return new record to AngularJS controller
  • invoke hub's method from controller
  • hub broadcasts call to clients appropriately

What I would like to do is bypass calling the hub's method from my AngularJS controller and call it directly from my API controller method.

This is what my hub currently looks like:

public class TaskHub : Hub
{
    public void InsertTask(TaskViewModel task)
    {
        Clients.Caller.onInsertTask(task, false);
        Clients.Others.onInsertTask(task, true);
    }
}

There are many SO threads out there on the topic, but everything I've read would have me adding the following code to my API controller:

var hubContext = GlobalHost.ConnectionManager.GetHubContext<TaskHub>();
hubContext.Clients.All.onInsertTask(task);

There are number of issues with this. First and foremost, I want the client broadcast calls to exist in a single class, not called directly from my API controller(s). Secondly, hubContext is an instance of IHubContext rather than IHubCallerConnectionContext. This means I only have access to all clients and couldn't broadcast different responses to the Caller and Others like I'm currently doing.

Is there a way to truly call a hub's method from C# and, ideally, have access to the different caller options? Ideally, I'd be able to do something as easy as the following from my API controller (or better yet, a solution with DI):

var taskHub = new TaskHub();
taskHub.InsertTask(task);

Thanks in advance.

SOLUTION

For posterity, I thought I'd include my full solution as per Wasp's suggestion.

First, I modified my javascript (AngularJS) service to include the SignalR connection ID in a custom request header for the API call, in this case an INSERT:

var addTask = function (task) {
    var config = { 
        headers: { 'ConnectionId': connection.id } 
    };
    return $http.post('/api/tasks', task, config);
};

Then, I retrieved the connection ID from the request in my API controller after performing the applicable CRUD operation and then called my hub:

public HttpResponseMessage Post(HttpRequestMessage request, [FromBody]TaskViewModel task)
{
    var viewModel = taskAdapter.AddTask(task);
    var connectionId = request.Headers.GetValues("ConnectionId").FirstOrDefault();
    TaskHub.InsertTask(viewModel, connectionId);
    return request.CreateResponse(HttpStatusCode.OK, viewModel);
}

My hub looks like this where I'm now only using static methods called from my API controller:

public class TaskHub : Hub
{
    private static IHubContext context = GlobalHost.ConnectionManager.GetHubContext<TaskHub>();

    public static void InsertTask(TaskViewModel task, string connectionId)
    {
        if (!String.IsNullOrEmpty(connectionId))
        {
            context.Clients.Client(connectionId).onInsertTask(task, false);
            context.Clients.AllExcept(connectionId).onInsertTask(task, true);
        }
        else
        {
            context.Clients.All.onInsertTask(task, true);
        }
    }
}

As you can see, I have a conditional statement in my hub method to handle if the hub call wasn't initiated from the client side portion of my app. This would be if an external app/service called my API. In such a situation, a SignalR connection and of course "ConnectionId" header value would not exist. In my case, though, I still would want to call the onInsertTask method for all connected clients which informs them of the data change. This should never happen, but I just included it for completeness.

Best Answer

In order to truly call a hub method, as you call it, you have to be connected to it, and call over that connection. By calling something different (your API) you cannot do that kind of call, and therefore you have to resort to the server initiated broadcasting capabilities, which by nature cannot know about what the Caller is because there's no SignalR's caller.

That said, if your client calling the API (no matter if it's Javascript or C#) is already connected to the hub when performing the call, you can always decorate your call towards the API with the connectionId of your hub's connection (by query string, by headers, ...). If your API receives that information, it can then simulate the Caller API with

Clients.Client(connectionId)

and it can do the same for Others with

Clients.AllExcept(connectionId)

over a IHubContext instance. Check the official docs.

You can then follow the suggestion from DDan about encapsulating the IHubContext usage in a convenient centralized way, or even restructure it a bit to make it easily DI-compliant.