页面刷新后SignalR事件监听器是否未正确释放?
Blazor InteractiveServer模式下SignalR连接未正确释放的问题
问题场景
我有一个采用InteractiveServer渲染模式的Blazor页面组件,代码中创建了SignalR HubConnection并实现IAsyncDisposable接口。页面通过按钮发送消息,服务器端仅将消息转发给所有客户端。
首次打开页面点击按钮时,页面显示1次消息,Debug输出窗口也显示1次;但刷新页面后再次点击,页面仅显示1次消息,Debug窗口却显示2次——追踪电路可发现GetMessage被前后两个电路调用,仿佛前一次的事件监听器未注销。
我已在DisposeAsync中移除监听器、停止并释放HubConnection,但问题仍存在,想了解事件处理器是否未正确释放,以及背后的逻辑误区。
编辑补充:尝试相关方案后刷新页面的输出情况——Debug窗口显示两次"Hello",分别对应不同的电路ID。
组件代码
@page "/test" @using System.Diagnostics @using Microsoft.AspNetCore.SignalR.Client @rendermode InteractiveServer @implements IAsyncDisposable <PageTitle>Test</PageTitle> <button @onclick="SendMessage">Send Message</button> @foreach(var m in _messages) { <p>@m</p> } @code { private readonly List<string> _messages = []; private HubConnection? _hubConnection; protected override async Task OnInitializedAsync() { _hubConnection = new HubConnectionBuilder() .WithUrl("http://localhost:5200/message") .WithAutomaticReconnect() .Build(); _hubConnection.On<string>("OnMessage", GetMessage); await _hubConnection.StartAsync(); await base.OnInitializedAsync(); } private void GetMessage(string message) { _messages.Add(message); Debug.WriteLine(message); InvokeAsync(StateHasChanged); } public async ValueTask DisposeAsync() { if (_hubConnection is not null) { _hubConnection.Remove("OnMessage"); await _hubConnection.StopAsync(); await _hubConnection.DisposeAsync(); } } private async Task SendMessage() { await _hubConnection.SendAsync("SendMessage", "Hello"); } }
服务器端Hub代码
public async Task SendMessage(string message) { await Clients.All.SendAsync("OnMessage", message); }
问题分析与解决
核心原因
- 电路延迟回收:InteractiveServer模式下,Blazor组件实例绑定到电路,刷新页面会创建新电路,但旧电路的回收是异步且延迟的——旧组件实例可能仍存活,其SignalR连接的事件处理器还在接收消息。
- 事件处理器移除不精准:
_hubConnection.Remove("OnMessage")仅按方法名移除,若底层委托引用匹配失败(比如委托被隐式包装),则无法正确移除旧的事件处理器。
修复方案
方案1:精准管理事件委托
注册时保存委托实例,移除时用该实例匹配,确保100%移除目标处理器:
@code { private readonly List<string> _messages = []; private HubConnection? _hubConnection; // 保存委托实例 private Action<string>? _messageHandler; protected override async Task OnInitializedAsync() { _hubConnection = new HubConnectionBuilder() .WithUrl("http://localhost:5200/message") .WithAutomaticReconnect() .Build(); _messageHandler = GetMessage; _hubConnection.On<string>("OnMessage", _messageHandler); await _hubConnection.StartAsync(); await base.OnInitializedAsync(); } private void GetMessage(string message) { _messages.Add(message); Debug.WriteLine(message); InvokeAsync(StateHasChanged); } public async ValueTask DisposeAsync() { if (_hubConnection is not null && _messageHandler is not null) { // 使用保存的委托实例移除 _hubConnection.Remove("OnMessage", _messageHandler); await _hubConnection.StopAsync(); await _hubConnection.DisposeAsync(); _messageHandler = null; _hubConnection = null; } } private async Task SendMessage() { await _hubConnection.SendAsync("SendMessage", "Hello"); } }
方案2:缩短电路超时(按需调整)
通过配置缩短旧电路的保留时间,加速旧组件实例的回收:
// Program.cs builder.Services.AddServerSideBlazor(options => { // 设置断开电路的保留时间为30秒(默认是5分钟) options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(30); });
方案3:依赖注入管理HubConnection
将HubConnection注册为Scoped服务(每个电路一个实例),利用DI生命周期自动管理连接的创建与释放:
// Program.cs注册服务 builder.Services.AddScoped(sp => { var navigationManager = sp.GetRequiredService<NavigationManager>(); return new HubConnectionBuilder() .WithUrl(navigationManager.ToAbsoluteUri("/message")) .WithAutomaticReconnect() .Build(); });
组件中注入使用:
@inject HubConnection HubConnection @implements IAsyncDisposable @code { private readonly List<string> _messages = []; private Action<string>? _messageHandler; protected override async Task OnInitializedAsync() { _messageHandler = GetMessage; HubConnection.On<string>("OnMessage", _messageHandler); if (HubConnection.State == HubConnectionState.Disconnected) { await HubConnection.StartAsync(); } await base.OnInitializedAsync(); } private void GetMessage(string message) { _messages.Add(message); Debug.WriteLine(message); InvokeAsync(StateHasChanged); } public async ValueTask DisposeAsync() { if (_messageHandler is not null) { HubConnection.Remove("OnMessage", _messageHandler); _messageHandler = null; } } private async Task SendMessage() { await HubConnection.SendAsync("SendMessage", "Hello"); } }
验证标准
修复后刷新页面,点击按钮时Debug窗口应仅显示一次消息,说明旧电路的事件处理器已被正确移除,旧连接已释放。
内容的提问来源于stack exchange,提问作者Rich
相关产品推荐
相关产品推荐

