从控制器调用SignalR Core Hub方法

96
如何从控制器调用SignalR Core Hub方法?我正在使用Microsoft.AspNetCore.SignalR(1.0.0-alpha2-final)和ASP.NET Core 2.0。我有一个与Excel,SolidEdge通信的Windows服务。当操作完成时,它会向我的ASP.NET Core应用程序中的控制器发布请求。现在我需要通过SignalR通知所有连接到服务器的客户端,外部程序已经完成了某些任务。我无法更改Window服务的工作方式(无法从Window服务连接到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);
    }
}
8个回答

133

解决方案1

另一种可能性是将您的HubContext注入到控制器中,例如:

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

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

那么你也可以打电话

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

但是这样你将会直接在所有客户端上调用方法。

解决方案2

你也可以使用类型化的Hubs: 简单地创建一个接口,定义服务器可以在客户端上调用哪些方法:

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

将已输入的 HubContext 注入到控制器中,并进行相关操作:

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

11
这个回答很棒,谢谢,但它并没有真正回答我的问题。现在可以这么做,但迟早我会需要调用Hub方法,而不是向所有客户端发送消息。 - Makla
@Makla:我修改了答案,请查看链接。 - Stephu
1
@Makla:我找到了一个更好的解决方案。请看第二个解决方案。我认为这会对你有所帮助。 - Stephu
14
但是有没有一种方法可以从控制器调用 .Send 方法?这样依赖注入看起来有点无用。理想情况下,我希望在我的 Hub 类中设置消息名称,而不必在许多不同的地方定义它们。但是要做到这一点,我需要能够调用 Hub 中的 Send 方法。 - adam3039
6
我没有这么做,我最终做的是将Hub上下文封装在一个叫做HubService的服务中,并为不同的调用方法构建了一个接口。至少这样做可以让所有内容集中在一个地方。 - adam3039
显示剩余5条评论

59

当前答案并没有回答提出的问题。

简单的答案是你不能直接从MVC控制器或其他地方调用hub方法。这是有意为之的设计。将hub视为包含SignalR Core客户端调用终点,而不是服务器或控制器方法的终点。

Microsoft在此处(这是针对预-SignalR Core文档的说明,但同样适用于SignalR Core)提到:

您不需要在服务器上的自己的代码中实例化Hub类或调用其方法; SignalR Hubs管道会为您完成所有这些操作。每当处理Hub操作时(例如客户端连接、断开连接或向服务器发出方法调用)SignalR会新建一个Hub类的实例。

由于Hub类的实例是短暂的,因此无法使用它们来在一次方法调用和下一次方法调用之间维护状态。 每当服务器从客户端接收到一个方法调用时,您的Hub类的新实例将处理消息。要通过多个连接和方法调用维护状态,请使用其他方法,例如数据库或Hub类上的静态变量或不派生自Hub的不同类。如果在内存中持久保存数据,如使用Hub类上的静态变量这样的方法,则应用程序域重新启动时数据将丢失。

如果您想从运行在Hub类外部的自己的代码向客户端发送消息,则不能通过实例化Hub类实例来完成,但可以通过获取对Hub类的SignalR上下文对象的引用来完成...

如果需要调用hub中的代码,则最好将其放入可从任何地方访问的外部类或服务中。

以下是使用ASP.NET Core的简单内置DI框架的示例:

假设您需要调用的代码位于DoStuff.cs中:

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

public interface IDoStuff
{
    string GetData();
}
在Startup.cs中,使用内置容器配置一个单例:
services.AddSingleton<IDoStuff, DoStuff>();

完整的 Startup.cs 如下所示:

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

对于您的中心类,请注入单例并在方法中使用:

public class MyHub : Hub
{
    private readonly IDoStuff _doStuff;

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

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

然后在您的控制器中注入IHubContext和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();
    }
}

当然,您的JavaScript或其他客户端应该配置一个show_data回调函数。

请注意,我们使用注入的Hub上下文将数据发送到所有SignalR客户端:_hub.Clients.All.SendAsync(...)


2
因此,我们是否需要从控制器和中心调用Clients.All方法?这似乎是多余的,我也想知道如何调用客户端方法。我们应该像通常一样在中心中调用它们吗? - user5871859
在Microsoft.AspNet.SignalR中使用这种方法怎么样?对于SignalR:如何在ASP.NET MVC中使用IHubContext<THub,T>接口?有什么想法吗? - user5871859
这只是接受答案的重复。常见的答案似乎是“我们不能调用hub的方法。我们应该直接通过“SendAsync”或通过类型化的hub方法调用客户端的方法”。 - Niksr

25

这现在已经有很好的文档记录,点击这里查看。

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

在Microsoft.AspNet.SignalR中使用这种方法怎么样?对于SignalR:如何在ASP.NET MVC中使用IHubContext<THub,T>接口?有什么想法吗? - user5871859
使用 GlobalHost 作为十六进制,就像在这里回答的那样 https://stackoverflow.com/questions/47902981/asp-net-core-with-signalr-static-socket - Andrius Naruševičius
@Stephu 在控制器中呼叫 Hub 方法是一个好习惯吗?起初我也是从我的控制器呼叫我的 Hub 方法,但后来为了“关注点分离”,我改为从客户端呼叫它们。您认为呢? - user5871859
这个运作良好。如果有很多客户端,"await _hubContext..." 会导致运行变慢吗? - Nick Chan Abdullah
@NickChanAbdullah 可以合理地假设如此。一个简单的解决方法是不等待发送,而是继续进行,因为它是一个异步操作。 - S.V.

6

这里有一种不使用注入的解决方案。

我设计了下面这样的中心类。

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

在您的Startup类中

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

}

因此,您可以从任何地方像这样使用它。

public class MyBizClass
{
    public void DoSomething()
    {
        NotificationHub.Current.MyMethod(...);
    }
}

NotificationHub.Current无法让您访问NotificationHub的实例,因此您无法在其上调用MyMethod。您可以访问它的context,从那里,您可以向其客户端发送消息(我想...我即将尝试)。这感觉真的很不可靠。 - Tatiana Racheva

2

可能的解决方案是使用C# Hub客户端。 您只需要创建一个新的HubConnection实例并使用它来调用所需的方法。这几乎与从javascript/typescript调用该方法相同。

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

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

PS: 我知道这有些过度,但这确实是对原问题的唯一正确答案。


2

由于我没有设置依赖注入,因此我在我的OWIN自托管应用程序中使用了这种方法。

可能不太美观,但客户端将在启动时调用Hub构造函数。

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

    public HostHub()
    {
        Instance = this;
    }

    public void BroadcastMessage(string message)
    {
        Clients.All.NewMessage(message);
    }
}

1

您可以向上下文添加扩展方法并调用它们。

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

+1. 这是正确的方法。实际上,我们不需要在中心自身中使用方法,所有方法都应该放到扩展中去。 - Niksr

0
可能的解决方案。
控制器。
[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);
    }
}

在HubClass中创建一个静态方法,该方法接收hub的上下文。
public class ChatHub : Hub<ITypedHubClient>
{
    public static async Task TaskCompleted(IHubContext<ChatHub> hubContext,int id)
    {
       await hubContext.Clients.All.InvokeAsync("Completed", id);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接