Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c#

signalr

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.

like image 831
im1dermike Avatar asked Jul 01 '15 18:07

im1dermike


People also ask

How does SignalR WebSocket work?

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.

Is SignalR asynchronous?

SignalR is an asynchronous signaling library for ASP.NET that our team is working on to help build real-time multi-user web application.

What is the SignalR hubs API?

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.

How do I instantiate the hub class in SignalR?

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.

How does SignalR work with client code?

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.

How do I define the SignalR hubs route using OWIN?

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.


1 Answers

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.

like image 159
Wasp Avatar answered Sep 22 '22 04:09

Wasp