You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.Net Core接收webhook回调后除SignalR外向指定客户端推消息的方案

解决方案整理

首先纠正一个认知误区:SignalR 中的 Hub 本身就是瞬态生命周期,每次客户端调用 Hub 方法、触发连接/断开事件时都会创建新的 Hub 实例,你原本将用户ID与连接ID的映射存在 Hub 的实例字段中本身就是错误的实现,哪怕不从控制器调用 Hub,多次触发 Hub 事件时也访问不到之前实例存储的映射数据。

下面是可直接落地的实现方案:

方案1:独立存储连接映射 + 注入IHubContext(最推荐,官方标准实现)

把连接映射从 Hub 中剥离,单独用单例服务存储,配合官方提供的 HubContext 即可实现精准推送:

  • 第一步:定义连接管理服务,用线程安全的并发字典存储映射关系
public interface IUserConnectionManager
{
    void AddConnection(string userId, string connectionId);
    void RemoveConnection(string connectionId);
    List<string> GetUserConnections(string userId);
}

public class UserConnectionManager : IUserConnectionManager
{
    private readonly ConcurrentDictionary<string, HashSet<string>> _userConnections = new();
    
    public void AddConnection(string userId, string connectionId)
    {
        _userConnections.AddOrUpdate(userId, 
            _ => new HashSet<string> { connectionId }, 
            (_, set) => { set.Add(connectionId); return set; });
    }

    public void RemoveConnection(string connectionId)
    {
        foreach (var (_, connections) in _userConnections)
        {
            if (connections.Remove(connectionId)) break;
        }
    }

    public List<string> GetUserConnections(string userId)
    {
        return _userConnections.TryGetValue(userId, out var connections) 
            ? connections.ToList() 
            : new List<string>();
    }
}
  • 第二步:在Program.cs中将连接管理服务注册为单例
builder.Services.AddSingleton<IUserConnectionManager, UserConnectionManager>();
  • 第三步:在 Hub 的连接/断开事件中更新映射
public class PaymentHub : Hub
{
    private readonly IUserConnectionManager _connManager;
    public PaymentHub(IUserConnectionManager connManager)
    {
        _connManager = connManager;
    }

    public override async Task OnConnectedAsync()
    {
        // 从当前登录用户Claims中取用户ID
        var userId = Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (!string.IsNullOrEmpty(userId))
        {
            _connManager.AddConnection(userId, Context.ConnectionId);
        }
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception exception)
    {
        _connManager.RemoveConnection(Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}
  • 第四步:在webhook控制器中注入依赖直接推送
[ApiController]
[Route("api/webhook/payment")]
public class PaymentWebhookController : ControllerBase
{
    private readonly IHubContext<PaymentHub> _hubContext;
    private readonly IUserConnectionManager _connManager;

    public PaymentWebhookController(IHubContext<PaymentHub> hubContext, IUserConnectionManager connManager)
    {
        _hubContext = hubContext;
        _connManager = connManager;
    }

    [HttpPost]
    public async Task<IActionResult> Callback([FromBody] PaymentCallbackDto dto)
    {
        // 校验回调合法性、拿到付款用户ID逻辑省略
        var userConnections = _connManager.GetUserConnections(dto.PaidUserId);
        foreach (var connectionId in userConnections)
        {
            await _hubContext.Clients.Client(connectionId).SendAsync("PaymentSuccess", new
            {
                OrderId = dto.OrderId,
                Amount = dto.Amount,
                PayTime = dto.PayTime
            });
        }
        return Ok();
    }
}

方案2:使用SignalR内置用户标识绑定(代码量最少)

如果你的前端连接SignalR和用户登录用的是同一套身份认证体系(比如JWT、Cookie认证),SignalR会自动将用户ID和连接绑定,不需要自己维护连接映射:

// 控制器中直接调用User方法指定推送对象即可
await _hubContext.Clients.User(dto.PaidUserId).SendAsync("PaymentSuccess", 通知数据);

方案3:分布式场景适配(多实例部署)

如果你的服务是多实例部署,用户连接可能分布在不同实例上,可以选择两种实现:

  • 配置SignalR Redis背板/使用云厂商托管的SignalR服务,直接用上面两种方案的代码即可跨实例推送,背板会自动路由到对应连接所在的实例
  • 不用背板的情况下,收到webhook回调后将通知数据写入Redis频道/RabbitMQ队列,所有服务实例订阅这个消息队列,收到消息后再用本地HubContext推送给当前实例上的对应用户

内容的提问来源于stack exchange,提问作者Jonny Mac

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 07:57:01