Blazor Server中SignalR HubConnection包装类SendAsync未触发Hub方法问题
Blazor HubConnection封装后SendAsync失效问题排查与修复
问题描述
在Blazor组件中封装HubConnection为HubConnectionService以方便测试,但调用SendAsync时,服务端Hub类对应逻辑未触发,无法向客户端推送消息。
封装的HubConnectionService代码
public class HubConnectionService : IHubConnectionService, IAsyncDisposable { private HubConnection _hubConnection; private readonly NavigationManager _navigationManager; public HubConnectionService(NavigationManager navigationManager) { _navigationManager = navigationManager; } public void Init(string path) { _hubConnection = new HubConnectionBuilder() .WithUrl(_navigationManager.ToAbsoluteUri(path)) .WithAutomaticReconnect() .Build(); } public void On<T>(string methodName, Func<T, Task> handler) => _hubConnection.On(methodName, handler); public void On(string methodName, Func<Task> handler) => _hubConnection.On(methodName, handler); public async Task SendAsync(string method, params object[] args) { await _hubConnection.SendAsync(method, args); } public async Task StartAsync() { await _hubConnection.StartAsync(); } public bool IsConnected => _hubConnection.State == HubConnectionState.Connected; public async ValueTask DisposeAsync() { if (_hubConnection is not null) { await _hubConnection.DisposeAsync(); } } }
组件代码
@page "/Jouer/{Id:int}" @attribute [Authorize()] @inject IGameService GameService @inject IHttpAccessorService HttpAccessorService @inject IReadyGameService ReadyGameService @inject NavigationManager Navigation @inject IHubConnectionService HubConnectionService @if (Game == null) { <Loader/> } else { <div class="container"> <Plate Game="Game" /> <div class="centered-container question-component"> @if (Game.Type == GameOnline && UsersReady != null && UsersReady.Any(d => d.Value == false)) { @if (!UserReady) { <div class=""> <InputCheckbox @bind-Value="UserReady" @onclick="() => UserIsReady()" /> Je suis prêt(e) </div> } else { <p>Vous avez indiqué que vous êtes prêt(e)!</p> } @foreach (var user in UsersReady) { <div class="d-flex align-items-center mb-2"> @if (user.Value) { <i class="bi bi-check-circle-fill me-2" style="color:green;font-size:1.5rem;"></i> } else { <i class="bi bi-x-circle-fill me-2" style="color:red;font-size:1.5rem;"></i> } <h4>@user.Key.Email</h4> </div> } } else { <GameQuestion Game="Game" GameChanged="OnGameChanged" /> } </div> </div> } @code { [Parameter] public int Id { get; set; } private GameDto Game { get; set; } private Dictionary<UserDto, bool> UsersReady { get; set; } = new(); private bool UserReady { get; set; } = false; protected override async Task OnInitializedAsync() { HubConnectionService.Init("/userhub"); HubConnectionService.On<string>("UserReadyChanged", async (userId) => { var userKey = UsersReady.Keys .First(u => u.Id == userId); UsersReady[userKey] = true; await InvokeAsync(StateHasChanged); }); await HubConnectionService.StartAsync(); await HubConnectionService.SendAsync("UpdateUserPath", Navigation.Uri); Game = await GameService.GetAsync(Id); RefreshUsersReady(); } private void RefreshUsersReady() { if (Game.Type == GameOnline) { var ready = ReadyGameService.GetAll(Id); UsersReady.Clear(); foreach (var player in Game.Players) { var dto = ready.FirstOrDefault(ready => ready.UserId == player.UserId); UsersReady.Add(player.User!, dto == null ? false : dto.Ready); } var userReady = UsersReady.FirstOrDefault(ur => ur.Key.Id == HttpAccessorService.GetConnectedUserId()); UserReady = userReady.Value == true ? true : false; } } private async Task OnGameChanged(GameDto game) { Game = game; await InvokeAsync(StateHasChanged); } private async Task UserIsReady() { var readyGame = new ReadyGameDto() { Game = Game, GameId = Id, UserId = HttpAccessorService.GetConnectedUserId() }; UserReady = true; await ReadyGameService.Add(readyGame); var userKey = UsersReady.Keys .First(u => u.Id == HttpAccessorService.GetConnectedUserId()); UsersReady[userKey] = true; await HubConnectionService.SendAsync("NotifyUserReady", Id, HttpAccessorService.GetConnectedUserId()); } }
问题根源
SendAsync参数传递错误
封装的SendAsync方法中,直接将params object[] args传给原生SendAsync时,整个数组会被当作单个参数传递,而非拆分独立参数。例如调用SendAsync("NotifyUserReady", Id, userId)时,服务端Hub方法实际收到的是[Id, userId]数组而非两个独立参数,导致方法签名匹配失败。服务生命周期配置错误
若HubConnectionService注册为单例,多组件调用Init会覆盖连接实例;若为瞬态,每次注入都会创建新连接,导致连接混乱。缺失连接状态检查
调用SendAsync前未验证连接是否就绪,可能在连接未建立时发送消息,导致消息丢失。
修复方案
1. 修正SendAsync参数传递
使用命名参数args: args确保数组被正确拆分为多个参数,同时添加连接状态检查与异常处理:
public async Task SendAsync(string method, params object[] args) { if (_hubConnection?.State != HubConnectionState.Connected) { throw new InvalidOperationException("Hub连接未建立,请先调用StartAsync"); } try { await _hubConnection.SendAsync(method, args: args); } catch (Exception ex) { // 可添加日志记录逻辑 Console.WriteLine($"Hub消息发送失败:{ex.Message}"); throw; } }
2. 配置正确的服务生命周期
将服务注册为Scoped,确保每个用户会话对应独立连接实例:
// Program.cs中注册服务 builder.Services.AddScoped<IHubConnectionService, HubConnectionService>();
3. 避免重复初始化连接
在Init方法中添加检查,防止重复创建连接:
public void Init(string path) { if (_hubConnection != null) { throw new InvalidOperationException("Hub连接已初始化,请勿重复调用"); } _hubConnection = new HubConnectionBuilder() .WithUrl(_navigationManager.ToAbsoluteUri(path)) .WithAutomaticReconnect() .Build(); }
4. 组件中确保连接启动顺序
在组件初始化时,先注册客户端事件,再启动连接,最后发送消息:
protected override async Task OnInitializedAsync() { HubConnectionService.Init("/userhub"); HubConnectionService.On<string>("UserReadyChanged", async (userId) => { var userKey = UsersReady.Keys.First(u => u.Id == userId); UsersReady[userKey] = true; await InvokeAsync(StateHasChanged); }); await HubConnectionService.StartAsync(); if (HubConnectionService.IsConnected) { await HubConnectionService.SendAsync("UpdateUserPath", Navigation.Uri); } Game = await GameService.GetAsync(Id); RefreshUsersReady(); }
5. 验证服务端Hub方法签名
确保服务端Hub方法参数与客户端传递的参数数量、类型一致:
public class UserHub : Hub { public async Task NotifyUserReady(int gameId, string userId) { // 向其他客户端推送消息 await Clients.Others.SendAsync("UserReadyChanged", userId); } }
内容的提问来源于stack exchange,提问作者mathdxdx
相关产品推荐
相关产品推荐

