从控制器调用 SignalR 核心集线器方法

如何从 Controller 调用 SignalR Core Hub 方法?
我正在使用 ASP.NET Core 2.0和 Microsoft. AspNetCore. SignalR (1.0.0-alpha2-final)。

我有窗口服务,通信与 Excel,SolidEdge... 当操作完成后,它后请求我的控制器在 ASP.NET 核心应用程序。现在我需要通知所有连接到 SignalR 服务器的客户端,外部程序完成了一些任务。
我无法改变窗口服务的工作方式。(无法从窗口服务连接到 SignalR)。
我为老 SignalR (GlobalHost.ConnectionManager.GetHubContext)找到了很多解决方案,但是很多都改变了,那些解决方案不再有效。

我的控制器:

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

我的中心:

public class VarDesignHub : Hub
{
public async Task TaskCompleted(int id)
{
await Clients.All.InvokeAsync("Completed", id);
}
}
88927 次浏览

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

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

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

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

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

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

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

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