74
votes

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);
    }
}
6

6 Answers

102
votes

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" };
    }
}
46
votes

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(...)

18
votes

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();
}
7
votes

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(...);
    }
}
1
votes

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

1
votes

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);
    }
}