Call SignalR Core Hub method from Controller
Asked Answered
B

8

98

How can I call SignalR Core Hub method from Controller?
I am using ASP.NET Core 2.0 with Microsoft.AspNetCore.SignalR (1.0.0-alpha2-final).

I have windows service which communicate with Excel, SolidEdge ... When operation is complete it post request to my controller in ASP.NET Core application. Now I need to inform all clients connected to server with SignalR that external program completed some task.
I can not change the way window service works. (Can not connect to SignalR from window service).
I found plenty solution for old SignalR (GlobalHost.ConnectionManager.GetHubContext), but much has changed and those solutions are not working anymore.

My controller:

[Route("API/vardesigncomm")]
public class VarDesignCommController : Controller
{
    [HttpPut("ProcessVarDesignCommResponse/{id}")]
    public async Task<IActionResult> ProcessVarDesignCommResponse(int id)
    {
        //call method TaskCompleted in Hub !!!! How?

        return new JsonResult(true);
    }
}

My hub:

public class VarDesignHub : Hub
{
    public async Task TaskCompleted(int id)
    {
        await Clients.All.InvokeAsync("Completed", id);
    }
}
Bathesda answered 24/10, 2017 at 7:31 Comment(0)
A
134

Solution 1

Another possibility is to inject your HubContext into your controller like:

public VarDesignCommController(IHubContext<VarDesignHub> hubcontext)
{
    HubContext = hubcontext;
    ...
}

private IHubContext<VarDesignHub> HubContext
{ get; set; }

Then you can also call

await this.HubContext.Clients.All.InvokeAsync("Completed", id);

But then you will direct call methods on all clients.

Solution 2

You can also work with typed hubs: Simple create an interface where you define which methods your server can call on the clients:

public interface ITypedHubClient
{
    Task BroadcastMessage(string name, string message);
}

Inherit from Hub:

public class ChatHub : Hub<ITypedHubClient>
{
    public void Send(string name, string message)
    {
        Clients.All.BroadcastMessage(name, message);
    }
}

Inject your the typed hubcontext into your controller, and work with it:

[Route("api/demo")]
public class DemoController : Controller
{
    IHubContext<ChatHub, ITypedHubClient> _chatHubContext;
    public DemoController(IHubContext<ChatHub, ITypedHubClient> chatHubContext)
    {
        _chatHubContext = chatHubContext;
    }

    // GET: api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        _chatHubContext.Clients.All.BroadcastMessage("test", "test");
        return new string[] { "value1", "value2" };
    }
}
Aalst answered 24/10, 2017 at 9:27 Comment(10)
This answer is great. Thanks, but it doesn't really answer my question. It will do it for now. But sooner or later I will need to call Hub method. Not send message to all clients.Bathesda
@Makla: I changed the answer see linkAalst
@Makla: I found a better solution. See solution 2. I think this will help you.Aalst
But is there a way to call the .Send method from the controller. The dependency injection seems a bit useless this way. Ideally I'd like to set message names in my Hub class, not having to define them in a bunch of different places. But to do that, I need to be able to call the Send method in the hubFriseur
@Friseur Did you find an example of creating methods in the hub and calling those?Twophase
@MarkRedman I did not, no. What I ended up doing was wrapping the Hub context in a service, say HubService, and then built an interface for the different invoke methods. At the very least it keeps it all in one place.Friseur
@Friseur I'm also looking for this. Is you hub calling your HubService?Cundiff
This answer is a workaround but it doesn't answer the question. My hub has a state that I want to take into account when calling the client, and I really don't want to take that logic out of the hub.Wyoming
@BaptisteCandellier: Is this comment there on the right question?Aalst
@Aalst this answer doesn't address the original question, of calling typed methods on the hub itself (not the hub client)Lowndes
T
61

The current answer doesn't answer the question posed.

The simple answer is you can't directly call a hub method from an MVC controller or elsewhere. This is by design. Think of the hub as containing the end points for SignalR Core clients to call, not for the server or controller methods.

Here's what Microsoft says (this is pre-SignalR Core documentation, but it still applies to SignalR Core):

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.

Because instances of the Hub class are transient, you can't use them to maintain state from one method call to the next. Each time the server receives a method call from a client, a new instance of your Hub class processes the message. To maintain state through multiple connections and method calls, use some other method such as a database, or a static variable on the Hub class, or a different class that does not derive from Hub. If you persist data in memory, using a method such as a static variable on the Hub class, the data will be lost when the app domain recycles.

If you want to send messages to clients from your own code that runs outside the Hub class, you can't do it by instantiating a Hub class instance, but you can do it by getting a reference to the SignalR context object for your Hub class...

If there is code in the hub that you need to call, it is better to put it into an external class or service that is accessible from anywhere.

So here's an example using the simple built-in DI framework for ASP.NET Core:

Assuming the code you need to call is in DoStuff.cs:

public class DoStuff : IDoStuff
{
    public string GetData()
    {
        return "MyData";
    }
}

public interface IDoStuff
{
    string GetData();
}

In Startup.cs, configure a singleton using the built-in container:

services.AddSingleton<IDoStuff, DoStuff>();

The full Startup.cs looks like this:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddSignalR();

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddSingleton<IDoStuff, DoStuff>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();
        app.UseSignalR(routes =>
        {
            routes.MapHub<MyHub>("/myhub");
        });

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

For your hub class, inject the singleton, and use it in a method:

public class MyHub : Hub
{
    private readonly IDoStuff _doStuff;

    public MyHub(IDoStuff doStuff)
    {
        _doStuff = doStuff;
    }

    public string GetData()
    {
       return  _doStuff.GetData();
    }
}

Then in your controller, inject the IHubContext and the singleton:

public class HomeController : Controller
{
    private readonly IDoStuff _doStuff;
    private readonly IHubContext<MyHub> _hub;

    public HomeController(IDoStuff doStuff, IHubContext<MyHub> hub)
    {
        _doStuff = doStuff;
        _hub = hub;
    }

    public async Task<IActionResult> Index()
    {
        var data = _doStuff.GetData();
        await _hub.Clients.All.SendAsync("show_data", data);

        return View();
    }
}

Of course, your Javascript or other client should have a show_data callback configured.

Notice we're using the injected hub context to send the data to all SignalR clients: _hub.Clients.All.SendAsync(...)

Tallie answered 29/10, 2018 at 17:1 Comment(3)
As a result, do we have to call Clients.All method from Controller and Hub? It seems to be redundant and I am also wondering how to call client side method. Should we call them in the Hub as generally we call?Jeanett
What about using this approach in Microsoft.AspNet.SignalR? Any idea on SignalR : How to use IHubContext<THub,T> Interface in ASP.NET MVC?Jeanett
This is just repetition of the accepted answer. It seems the common answer is "We can't call hub's methods. We should call client's methods instead, either directly through "SendAsync" or through typed hub's methods"Riplex
S
26

This is now well-documented here

You can inject an instance of IHubContext into a controller by adding it to your constructor:

public class HomeController : Controller
{
    private readonly IHubContext<NotificationHub> _hubContext;

    public HomeController(IHubContext<NotificationHub> hubContext)
    {
        _hubContext = hubContext;
    }
}

Now, with access to an instance of IHubContext, you can call hub methods as if you were in the hub itself.

public async Task<IActionResult> Index()
{
    await _hubContext.Clients.All.SendAsync("Notify", $"Home page loaded at: {DateTime.Now}");
    return View();
}
Shoa answered 23/6, 2019 at 15:1 Comment(5)
What about using this approach in Microsoft.AspNet.SignalR? Any idea on SignalR : How to use IHubContext<THub,T> Interface in ASP.NET MVC?Jeanett
@hexadecimal use GlobalHost as answered here #47903481Abraxas
@Aalst Is it good habit to call Hub method from Controller? At first I called my hub method from my Controller as well, but then I called them from client side in order to separate of concerns. What do you think?Jeanett
this works well. if there are many clients, would "await _hubContext..." cause it to run slower ?Nonsuit
@NickChanAbdullah It would be reasonable to assume so. A simple fix for that would be not to await the send, and simply continue, as it's an async operation.Shoa
T
6

Another answer not use injection is here.

I design my hub class like below.

public class NotificationHub : Microsoft.AspNetCore.SignalR.Hub
{
    public static IHubContext<NotificationHub> Current { get; set; }
}

In your Startup class

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    NotificationHub.Current = app.ApplicationServices.GetService<IHubContext<NotificationFromServerHub>>();

}

So, you can use as like this from anywhere.

public class MyBizClass
{
    public void DoSomething()
    {
        NotificationHub.Current.MyMethod(...);
    }
}
Thuggee answered 14/4, 2020 at 18:14 Comment(1)
NotificationHub.Current does not give you access to an instance of NotificationHub, so you can't call MyMethod on it. You get access to its context, and from there, you can send messages to its clients (I think... I'm about to try). This feels really hackyBani
M
2

Possible solution is to use C# hub client. You only have to create a new HubConnection instance and use it to invoke the required method. It is almost the same as calling the method from javascript/typescript.

using (var hubConnection = new HubConnection("http://www.contoso.com/")) 
{
    IHubProxy hubproxy = hubConnection.CreateHubProxy("MyHub");

    hubproxy.Invoke("TaskCompleted", id);
)

PS: I know it is overkill, but it is really only correct answer to the original question

Mandola answered 4/3, 2020 at 18:2 Comment(0)
C
2

I used this approach for my OWIN self hosted application as I don't have dependency injection set up.

It might be ugly but the clients will call the Hub constructors when they startup.

public class HostHub : Hub
{
    public static HostHub Instance { get; private set; }

    public HostHub()
    {
        Instance = this;
    }

    public void BroadcastMessage(string message)
    {
        Clients.All.NewMessage(message);
    }
}
Contribute answered 11/2, 2021 at 1:0 Comment(0)
O
2

You can add extension methods to your context and call them.

IExampleHubClient.cs

public interface IExampleHubClient
{
    void ExampleMethod();
}

ExampleHub.cs

[Authorize]
public class ExampleHub : Hub<IExampleHubClient>
{
    public override async Task OnConnectedAsync()
    {
        var userId = Context.User!.Identity!.Name!;
        await Groups.AddToGroupAsync(Context.ConnectionId, userId);
        await base.OnConnectedAsync();
    }
}

ExampleHubExtensions.cs

public static class ExampleHubExtensions
{
    public static void ExampleMethod(this IHubContext<ExampleHub, IExampleHubClient> context, IEnumerable<string> userIds)
    {
        context.Clients.Groups(userIds).ExampleMethod();
    }
}

ExampleController.cs

[ApiController, Route("[controller]/[action]")]
public class ExampleController : Controller
{
    private readonly IHubContext<ExampleHub, IExampleHubClient> _context;

    public ExampleController(IHubContext<ExampleHub, IExampleHubClient> context)
    {
        _context = context;
    }

    [HttpGet]
    public IActionResult ExampleAction()
    {
        _context.ExampleMethod(new string[] { "1234", "2345" });
        return Ok();
    }
}
Olney answered 12/11, 2022 at 17:12 Comment(1)
+1. This is way to go. We actually don't need methods in the hub itself, all of them go to the extensions.Riplex
C
0

Posible solution.

The Controller

[Route("API/vardesigncomm")]
public class VarDesignCommController : Controller
{
    private IHubContext<ChatHub> _hubContext;
    public VarDesignCommController (IHubContext<ChatHub> hubContext){
       _hubContext=hubContext
    }

    [HttpPut("ProcessVarDesignCommResponse/{id}")]
    public async Task<IActionResult> ProcessVarDesignCommResponse(int id)
    {
       //call method TaskCompleted in Hub !!!! How?
       await ChatHub.TaskCompleted(_hubContext,id);
       return new JsonResult(true);
    }
}

Create static method in HubClass which receives the context of the hub.

public class ChatHub : Hub<ITypedHubClient>
{
    public static async Task TaskCompleted(IHubContext<ChatHub> hubContext,int id)
    {
       await hubContext.Clients.All.InvokeAsync("Completed", id);
    }
}
Caducous answered 17/9, 2021 at 22:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.