SignalR .NET7中OnConnectedAsync内调用InvokeAsync报错问题
问题场景
在使用.NET 7的SignalR Client Results功能时触发错误:"Client results inside OnConnectedAsync Hub methods are not allowed"。业务逻辑是通过OnConnectedAsync方法缓存客户端的Connection ID,后续通过该ID向特定客户端发送消息并等待响应。
触发异常的代码
调用客户端方法时抛出异常的代码行:
return await Clients.Client(connectionMetadata.ConnectionId).InvokeAsync<bool>("CreateConnection", message, CancellationToken.None);
完整Hub类代码:
public class ConnectionCreationHub : Hub { private readonly List<ConnectionMetadata> connectionIdsCache; private readonly ITokenValidator2 validator; public ConnectionCreationHub( ITokenValidator2 validator, List<ConnectionMetadata> connectionIdsCache) { this.validator = validator; this.connectionIdsCache = connectionIdsCache; } public async Task<bool> SendCreateConnectionMessage( Guid userId, string message) { var connectionMetadata = connectionIdsCache .FirstOrDefault(entry => entry.UserId.Equals(userId)); if (connectionMetadata == null) { throw new Exception($"Client with userId {userId} not connected"); } // 此行抛出错误:System.InvalidOperationException: Client results inside OnConnectedAsync Hub methods are not allowed. return await Clients.Client(connectionMetadata.ConnectionId).InvokeAsync<bool>("CreateConnection", message, CancellationToken.None); } public override async Task OnConnectedAsync() { // 验证授权令牌并缓存Connection ID var tokenClaims = await this.AuthorizeOrThrow(validator); var userId = tokenClaims.UserId; connectionIdsCache.Add(new(userId, Context.ConnectionId)); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { // 客户端断开连接时清理缓存 var connectionMetadata = connectionIdsCache .FirstOrDefault(entry => entry.ConnectionId == Context.ConnectionId); if (connectionMetadata != null) { connectionIdsCache.Remove(connectionMetadata); } await base.OnDisconnectedAsync(exception); } }
异常详情
仅使用InvokeAsync时触发错误,SendAsync可正常运行,异常信息如下:
System.InvalidOperationException HResult=0x80131509 Message=Client results inside OnConnectedAsync Hub methods are not allowed. Source=Microsoft.AspNetCore.SignalR.Core StackTrace: at Microsoft.AspNetCore.SignalR.Internal.HubCallerClients.NoInvokeSingleClientProxy.InvokeCoreAsync[T](String method, Object[] args, CancellationToken cancellationToken) at Microsoft.AspNetCore.SignalR.ClientProxyExtensions.InvokeAsync[T](ISingleClientProxy clientProxy, String method, Object arg1, CancellationToken cancellationToken) at ConnectionCreationHub.<SendCreateConnectionMessage>d__3.MoveNext() in *\ConnectionCreationHub.cs:line 42
解决方案
1. 调整缓存添加时机
将Connection ID的缓存操作放到base.OnConnectedAsync()之后执行,确保客户端连接完全建立后再记录信息:
public override async Task OnConnectedAsync() { var tokenClaims = await this.AuthorizeOrThrow(validator); var userId = tokenClaims.UserId; // 先完成SignalR基础连接流程 await base.OnConnectedAsync(); // 再缓存Connection ID connectionIdsCache.Add(new(userId, Context.ConnectionId)); }
2. 确保调用时机正确
避免在客户端连接初始化阶段(OnConnectedAsync执行过程中)调用SendCreateConnectionMessage方法。要求客户端在连接成功后(例如收到连接成功的通知),再发起对该Hub方法的调用,此时客户端已完全就绪,可以处理需要返回结果的InvokeAsync请求。
3. 替换线程不安全的缓存容器
当前使用的List<ConnectionMetadata>不是线程安全的,多客户端并发连接时可能出现数据冲突。建议改用ConcurrentDictionary<Guid, ConnectionMetadata>存储用户与Connection ID的映射:
// 注入时替换为ConcurrentDictionary private readonly ConcurrentDictionary<Guid, ConnectionMetadata> connectionIdsCache; // 添加缓存的代码改为 connectionIdsCache.TryAdd(userId, new(userId, Context.ConnectionId)); // 清理缓存的代码改为 connectionIdsCache.TryRemove(userId, out _);
4. 调整交互模式(可选)
如果需要在客户端连接时触发需要响应的操作,可以改为客户端主动发起请求的模式:客户端连接完成后,主动调用Hub的某个方法,服务器再根据业务逻辑返回结果或触发后续流程,避免服务器在连接初始化阶段主动调用客户端的InvokeAsync。
内容的提问来源于stack exchange,提问作者Nasri Yatim

