ASP.NET Core控制器DI中,如何在Hub外使用SignalR的Clients.Caller与Clients.Others?
在ASP.NET Core SignalR的Hub外部模拟Clients.Caller和Clients.Others的方法
这个问题其实挺典型的——毕竟IHubContext是全局的,不像Hub类内部自带当前连接的上下文,所以直接拿不到Caller和Others。不过咱们可以通过明确指定连接ID的方式来模拟这两个功能,核心思路是:
Clients.Caller本质就是「当前发起调用的连接客户端」,对应Clients.Client(特定ConnectionId)Clients.Others本质就是「除了当前连接之外的所有客户端」,对应Clients.AllExcept(特定ConnectionId)
下面分两种场景给你具体的实现方案:
场景1:从Hub方法触发外部逻辑,需要传递Caller上下文
如果你的业务逻辑是从Hub的某个方法里触发的,那可以直接在Hub里拿到当前连接的ConnectionId,再把它传给外部服务(比如业务服务),服务里用IHubContext就可以模拟Caller和Others了。
代码示例:
首先定义业务服务:
public interface INotificationService { Task SendToCallerAndOthers(string callerConnectionId); } public class NotificationService : INotificationService { private readonly IHubContext<NotificationHub> _hubContext; public NotificationService(IHubContext<NotificationHub> hubContext) { _hubContext = hubContext; } public async Task SendToCallerAndOthers(string callerConnectionId) { // 模拟 Clients.Caller:给调用者的连接发送消息 await _hubContext.Clients.Client(callerConnectionId).SendAsync("aMethod"); // 模拟 Clients.Others:给除了调用者之外的所有客户端发送消息 await _hubContext.Clients.AllExcept(callerConnectionId).SendAsync("aMethod"); } }
然后修改Hub类,注入服务并传递ConnectionId:
public class NotificationHub : Hub { private readonly INotificationService _notificationService; public NotificationHub(INotificationService notificationService) { _notificationService = notificationService; } public async Task Test() { // 拿到当前调用者的ConnectionId,传给服务 await _notificationService.SendToCallerAndOthers(Context.ConnectionId); } }
场景2:在API控制器等Hub外部主动触发通知
如果是在完全独立的Hub外部(比如API控制器),你需要先知道目标客户端的ConnectionId——通常可以把用户和ConnectionId的映射关系存在缓存(比如Redis、MemoryCache)或者数据库里,当需要发送时取出对应的ID即可。
代码示例:
首先定义一个连接映射服务,用来存储用户和ConnectionId的对应关系:
public interface IConnectionMappingService { void AddConnection(string userId, string connectionId); void RemoveConnection(string connectionId); string GetConnectionId(string userId); } public class ConnectionMappingService : IConnectionMappingService { private readonly Dictionary<string, string> _connections = new(); private readonly object _lock = new(); public void AddConnection(string userId, string connectionId) { lock (_lock) { _connections[userId] = connectionId; } } public void RemoveConnection(string connectionId) { lock (_lock) { var entry = _connections.FirstOrDefault(kv => kv.Value == connectionId); if (entry.Key != null) { _connections.Remove(entry.Key); } } } public string GetConnectionId(string userId) { lock (_lock) { _connections.TryGetValue(userId, out var connectionId); return connectionId; } } }
然后在Hub里维护这个映射(客户端连接/断开时更新):
public class NotificationHub : Hub { private readonly IConnectionMappingService _connectionMappingService; public NotificationHub(IConnectionMappingService connectionMappingService) { _connectionMappingService = connectionMappingService; } public override async Task OnConnectedAsync() { // 假设从Claims里获取当前用户ID,你可以根据自己的认证方式调整 var userId = Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { _connectionMappingService.AddConnection(userId, Context.ConnectionId); } await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { _connectionMappingService.RemoveConnection(Context.ConnectionId); await base.OnDisconnectedAsync(exception); } }
最后在API控制器里使用:
[Route("api/[controller]")] [ApiController] public class AController : ControllerBase { private readonly IHubContext<NotificationHub> _hubContext; private readonly IConnectionMappingService _connectionMappingService; public AController(IHubContext<NotificationHub> hubContext, IConnectionMappingService connectionMappingService) { _hubContext = hubContext; _connectionMappingService = connectionMappingService; } [HttpPost("send-notification")] public async Task<IActionResult> SendNotification(string targetUserId) { var targetConnectionId = _connectionMappingService.GetConnectionId(targetUserId); if (string.IsNullOrEmpty(targetConnectionId)) { return NotFound("目标用户未连接到SignalR"); } // 模拟 Clients.Caller:给目标用户发送消息 await _hubContext.Clients.Client(targetConnectionId).SendAsync("aMethod"); // 模拟 Clients.Others:给除了目标用户之外的所有客户端发送消息 await _hubContext.Clients.AllExcept(targetConnectionId).SendAsync("aMethod"); return Ok("通知发送成功"); } }
关键总结
Clients.Caller和Clients.Others都是基于当前连接上下文的快捷方式,而IHubContext是全局的,没有绑定到特定连接,所以必须显式指定ConnectionId才能模拟。- 只要能拿到目标的ConnectionId,就可以通过
Clients.Client(id)和Clients.AllExcept(id)实现完全等价的功能。
内容的提问来源于stack exchange,提问作者Natalie Perret
相关产品推荐
相关产品推荐

