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

SignalR Hub中Clients调用触发System.ObjectDisposedException异常排查

排查System.ObjectDisposedException异常及完整SignalR消息推送实现方案

首先,我们来拆解你遇到的System.ObjectDisposedException异常原因,然后一步步修复并完善你的ChatHub实现。

一、异常核心原因分析

你的代码里有几个关键问题会导致这个异常,以及在线状态管理的逻辑漏洞:

  1. 线程不安全的在线用户列表
    你使用了静态List<string> online_users来管理在线用户,但List<T>不是线程安全的集合。当多个用户同时连接/断开时,会出现竞态条件:比如用户已经离线,但列表里还残留着用户名,导致你判断用户在线(check3 = online_users.Contains(recieverId)),但实际该用户的所有连接已经断开,此时调用Clients.Group(recieverId).RecievedMessage就会尝试操作已释放的连接对象,触发ObjectDisposedException。

  2. 异步操作未等待(await)

    • 在OnConnectedAsync和OnDisconnectedAsync中,Groups.AddToGroupAsync和Groups.RemoveFromGroupAsync都是异步方法,但你没有使用await等待操作完成。这会导致组的状态更新不及时,比如用户还没加入组就尝试发消息,或者用户已经断开但组里还残留连接,后续操作时就会访问已释放的资源。
    • fetchPendingMessages中调用SendMessageAsync时没有await,会导致消息发送逻辑未完成就继续执行,可能引发资源释放问题。
  3. 错误的方法返回类型
    OnConnectedAsync的返回类型被错误定义为Task<Task>,正确的重写应该是Task。这个错误会导致异步执行顺序混乱,Hub的初始化逻辑可能还没完成就处理后续请求。

  4. 冗余的组管理
    其实SignalR已经内置了基于用户身份的连接管理,你不需要手动给每个用户创建同名组。使用Clients.User(recieverId)可以直接向该用户的所有在线连接发送消息,比手动维护组更可靠。

二、修复后的完整ChatHub实现

下面是修复并优化后的代码,解决了异常问题,同时完善了消息推送逻辑:

using IdentityCore.DbLayer.Entity;
using IdentityCore.Interface;
using IdentityCore.Services.Contracts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
using System.Diagnostics;

namespace IdentityCore.Hubs
{
    [Authorize]
    public class ChatHub : Hub<IChatHub>
    {
        // 使用线程安全的集合存储用户的连接ID集合(支持多设备登录)
        private static readonly ConcurrentDictionary<string, HashSet<string>> _userConnections = new();
        private readonly IUserContract _userContract;

        public ChatHub(IUserContract userContract)
        {           
             _userContract = userContract;
        }

        // 修复返回类型为Task,并正确await所有异步操作
        public override async Task OnConnectedAsync()
        {
            var userName = Context.User.Identity.Name;
            var connectionId = Context.ConnectionId;

            // 线程安全地添加用户连接
            _userConnections.AddOrUpdate(
                userName,
                _ => new HashSet<string> { connectionId },
                (_, existingConnections) =>
                {
                    lock (existingConnections)
                    {
                        existingConnections.Add(connectionId);
                        return existingConnections;
                    }
                });

            // 拉取离线消息
            await FetchPendingMessages();
            
            await base.OnConnectedAsync();
        }

        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            var userName = Context.User.Identity.Name;
            var connectionId = Context.ConnectionId;

            // 线程安全地移除用户连接
            if (_userConnections.TryGetValue(userName, out var connections))
            {
                lock (connections)
                {
                    connections.Remove(connectionId);
                    if (connections.Count == 0)
                    {
                        _userConnections.TryRemove(userName, out _);
                    }
                }
            }

            await base.OnDisconnectedAsync(exception);
        }

        public void Greetings()
        {
            Clients.Caller.DisplayGreeting($"You are now connected with connectionId:- {Context.ConnectionId}");
        }

        public async Task FetchPendingMessages()
        {
            var userName = Context.User.Identity.Name;
            var pendingMessages = await _userContract.GetTempMessages(userName);
            
            foreach (var message in pendingMessages)
            {
                // 等待消息发送完成,避免资源泄漏
                await SendMessageAsync(message.RecieverId, message.SenderId, message.SenderUsername, message.Message);
                // 可选:发送完成后从数据库删除离线消息
                await _userContract.RemoveTempMessage(message.Id);
            }
        }

        public async Task SendMessageAsync(string recieverId, string senderId, string senderUsername, string message)
        {
            // 验证发送方和接收方存在
            var receiverExists = await _userContract.CheckIfUserExists(recieverId);
            var senderExists = await _userContract.CheckIfUserExists(senderId);
            
            if (!receiverExists || !senderExists)
            {
                // 可选:通知发送方用户不存在
                await Clients.Caller.DisplayError("Target user does not exist.");
                return;
            }

            // 判断接收方是否在线(通过连接集合是否存在)
            var isReceiverOnline = _userConnections.ContainsKey(recieverId);
            
            if (isReceiverOnline)
            {
                // 使用SignalR内置的用户发送方法,自动推送到该用户所有在线连接
                await Clients.User(recieverId).RecievedMessage(senderId, senderUsername, message);
            }
            else
            {
                // 离线用户存储消息到数据库
                await _userContract.AddTempMessage(recieverId, senderId, senderUsername, message);
            }
        }
    }
}

三、关键优化点说明

  1. 线程安全的连接管理
    使用ConcurrentDictionary<string, HashSet<string>>存储每个用户的所有连接ID,确保多线程环境下的安全操作,同时支持用户多设备在线的场景。

  2. 移除冗余的组操作
    利用SignalR的Clients.User(userId)方法,直接向目标用户的所有在线连接发送消息,无需手动维护组,减少出错概率。

  3. 正确处理异步操作
    所有异步方法都添加了await,确保操作顺序正确,避免资源未初始化或已释放的问题。

  4. 完善离线消息处理
    在拉取离线消息后,建议删除已推送的消息(避免重复推送),并添加了用户不存在的错误处理。

四、额外注意事项

  • 确保你的IChatHub接口中正确定义了RecievedMessage、DisplayGreeting、DisplayError等客户端方法,客户端代码要实现这些方法来接收消息。
  • 如果你的用户ID和用户名不是同一个值,要注意在Context.User中获取正确的用户ID(比如通过Context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value),而不是Identity.Name。
  • 生产环境中,建议使用分布式缓存(如Redis)来存储用户连接信息,避免应用重启后丢失在线状态。

内容的提问来源于stack exchange,提问作者Smarpan Sharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 16:56:14