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:
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.
SignalR is an abstraction over some of the transports that are required to do real-time work between client and server. SignalR first attempts to establish a WebSocket connection if possible. WebSocket is the optimal transport for SignalR because it has: The most efficient use of server memory.
SignalR is an asynchronous signaling library for ASP.NET that our team is working on to help build real-time multi-user web application.
The SignalR Hubs API enables you to make remote procedure calls (RPCs) from a server to connected clients and from clients to the server. In server code, you define methods that can be called by clients, and you call methods that run on the client.
You don't instantiate the Hub class or call its methods from your own code on the server; all that is done for you by the SignalR Hubs pipeline. SignalR creates a new instance of your Hub class each time it needs to handle a Hub operation such as when a client connects, disconnects, or makes a method call to the server.
In client code, you define methods that can be called from the server, and you call methods that run on the server. SignalR takes care of all of the client-to-server plumbing for you.
To define the route that clients will use to connect to your Hub, call the MapSignalR method when the application starts. MapSignalR is an extension method for the OwinExtensions class. The following example shows how to define the SignalR Hubs route using an OWIN startup class.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With