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

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());
    }
}

问题根源

  1. SendAsync参数传递错误
    封装的SendAsync方法中,直接将params object[] args传给原生SendAsync时,整个数组会被当作单个参数传递,而非拆分独立参数。例如调用SendAsync("NotifyUserReady", Id, userId)时,服务端Hub方法实际收到的是[Id, userId]数组而非两个独立参数,导致方法签名匹配失败。

  2. 服务生命周期配置错误
    若HubConnectionService注册为单例,多组件调用Init会覆盖连接实例;若为瞬态,每次注入都会创建新连接,导致连接混乱。

  3. 缺失连接状态检查
    调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 12:29:53