【发布时间】:2021-02-03 12:54:48
【问题描述】:
我是一名新用户,正在努力从 ASP.NET Core Blazor 服务器页面优雅地关闭辅助 signalR 客户端。
我在第一次渲染 Blazor 服务器页面时设置辅助 signalR 客户端连接。当通过浏览器选项卡关闭页面时,我正在尝试关闭此辅助 signalR 客户端连接。
在撰写本文时,通过浏览器选项卡关闭页面时似乎没有触发DisposeAsync。但是,Dispose 方法 IS 触发了。此外,在 Safari 13.0.5 中,关闭浏览器选项卡时不会触发 Dispose 方法? Opera、Firefox 和 Chrome 都在关闭浏览器选项卡时触发了Dispose。通过 macOS Catalina v10.15.7 将 Safari 更新到 v14.0 (15610.1.28.9, 15610) 解决了这个问题。
目前,我正在从Dispose 调用DisposeAsync 以关闭signalR 连接。我正在使用以下代码关闭客户端连接:
...
Logger.LogInformation("Closing secondary signalR connection...");
await hubConnection.StopAsync();
Logger.LogInformation("Closed secondary signalR connection");
...
StopAsync 方法似乎被阻止,即没有为 “关闭的辅助信号 R 连接”输出消息。虽然,我的服务器集线器的 OnDisconnectedAsync 处理程序显示连接已断开。这类似于issue 中描述的behaviour。
如何在 ASP.NET Core 3.1 中正确处理 signalR 连接?
完整的代码清单如下:
signalR 连接的处理
#region Dispose
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Clear secondary signalR Closed event handler and stop the
/// secondary signalR connection
/// </summary>
/// <remarks>
/// ASP.NET Core Release Candidate 5 calls DisposeAsync when
/// navigating away from a Blazor Server page. Until the
/// release is stable DisposeAsync will have to be triggered from
/// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
/// in Dispose().
/// However, providing DisposeAsync() now makes the migration easier
/// https://github.com/dotnet/aspnetcore/issues/26737
/// https://github.com/dotnet/aspnetcore/issues/9960
/// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
/// </remarks>
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing)
{
Logger.LogInformation("Index.razor page is disposing...");
try
{
if (hubConnection != null)
{
Logger.LogInformation("Removing signalR client event handlers...");
hubConnection.Closed -= CloseHandler;
}
// Until ASP.NET Core 5 is released in November
// trigger DisposeAsync(). See docstring and DiposeAsync() below.
// not ideal, but having to use GetAwaiter().GetResult() until
// forthcoming release of ASP.NET Core 5 for the introduction
// of triggering DisposeAsync on pages that implement IAsyncDisposable
DisposeAsync().GetAwaiter().GetResult();
}
catch (Exception exception)
{
Logger.LogError($"Exception encountered while disposing Index.razor page :: {exception.Message}");
}
}
disposed = true;
}
/// <summary>
/// Dispose the secondary backend signalR connection
/// </summary>
/// <remarks>
/// ASP.NET Core Release Candidate 5 adds DisposeAsync when
/// navigating away from a Blazor Server page. Until the
/// release is stable DisposeAsync will have to be triggered from
/// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
/// in Dispose().
/// However, providing DisposeAsync() now makes the migration easier
/// https://github.com/dotnet/aspnetcore/issues/26737
/// https://github.com/dotnet/aspnetcore/issues/9960
/// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
/// </remarks>
public async virtual ValueTask DisposeAsync()
{
try
{
if (hubConnection != null)
{
Logger.LogInformation("Closing secondary signalR connection...");
await hubConnection.StopAsync();
Logger.LogInformation("Closed secondary signalR connection");
}
// Dispose(); When migrated to ASP.NET Core 5 let DisposeAsync trigger Dispose
}
catch (Exception exception)
{
Logger.LogInformation($"Exception encountered wwhile stopping secondary signalR connection :: {exception.Message}");
}
}
#endregion
Blazor 服务器页面的完整代码
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using WebApp.Data;
using WebApp.Data.Serializers.Converters;
using WebApp.Data.Serializers.Converters.Visitors;
using WebApp.Repository.Contracts;
namespace WebApp.Pages
{
public partial class Index : IAsyncDisposable, IDisposable
{
private HubConnection hubConnection;
public bool IsConnected => hubConnection.State == HubConnectionState.Connected;
private bool disposed = false;
[Inject]
public NavigationManager NavigationManager { get; set; }
[Inject]
public IMotionDetectionRepository Repository { get; set; }
[Inject]
public ILogger<MotionDetectionConverter> LoggerMotionDetection { get; set; }
[Inject]
public ILogger<MotionInfoConverter> LoggerMotionInfo { get; set; }
[Inject]
public ILogger<JsonVisitor> LoggerJsonVisitor { get; set; }
[Inject]
public ILogger<Index> Logger { get; set; }
#region Dispose
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Clear secondary signalR Closed event handler and stop the
/// secondary signalR connection
/// </summary>
/// <remarks>
/// ASP.NET Core Release Candidate 5 calls DisposeAsync when
/// navigating away from a Blazor Server page. Until the
/// release is stable DisposeAsync will have to be triggered from
/// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
/// in Dispose().
/// However, providing DisposeAsync() now makes the migration easier
/// https://github.com/dotnet/aspnetcore/issues/26737
/// https://github.com/dotnet/aspnetcore/issues/9960
/// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
/// </remarks>
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing)
{
Logger.LogInformation("Index.razor page is disposing...");
try
{
if (hubConnection != null)
{
Logger.LogInformation("Removing signalR client event handlers...");
hubConnection.Closed -= CloseHandler;
}
// Until ASP.NET Core 5 is released in November
// trigger DisposeAsync(). See docstring and DiposeAsync() below.
// not ideal, but having to use GetAwaiter().GetResult() until
// forthcoming release of ASP.NET Core 5 for the introduction
// of triggering DisposeAsync on pages that implement IAsyncDisposable
DisposeAsync().GetAwaiter().GetResult();
}
catch (Exception exception)
{
Logger.LogError($"Exception encountered while disposing Index.razor page :: {exception.Message}");
}
}
disposed = true;
}
/// <summary>
/// Dispose the secondary backend signalR connection
/// </summary>
/// <remarks>
/// ASP.NET Core Release Candidate 5 adds DisposeAsync when
/// navigating away from a Blazor Server page. Until the
/// release is stable DisposeAsync will have to be triggered from
/// Dispose. Sadly, this means having to use GetAwaiter().GetResult()
/// in Dispose().
/// However, providing DisposeAsync() now makes the migration easier
/// https://github.com/dotnet/aspnetcore/issues/26737
/// https://github.com/dotnet/aspnetcore/issues/9960
/// https://github.com/dotnet/aspnetcore/milestone/57?closed=1
/// </remarks>
public async virtual ValueTask DisposeAsync()
{
try
{
if (hubConnection != null)
{
Logger.LogInformation("Closing secondary signalR connection...");
await hubConnection.StopAsync();
Logger.LogInformation("Closed secondary signalR connection");
}
// Dispose(); When migrated to ASP.NET Core 5 let DisposeAsync trigger Dispose
}
catch (Exception exception)
{
Logger.LogInformation($"Exception encountered wwhile stopping secondary signalR connection :: {exception.Message}");
}
}
#endregion
#region ComponentBase
/// <summary>
/// Connect to the secondary signalR hub after rendering.
/// Perform on the first render.
/// </summary>
/// <remarks>
/// This could have been performed in OnInitializedAsync but
/// that method gets executed twice when server prerendering is used.
/// </remarks>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var hubUrl = NavigationManager.BaseUri.TrimEnd('/') + "/motionhub";
try
{
Logger.LogInformation("Index.razor page is performing initial render, connecting to secondary signalR hub");
hubConnection = new HubConnectionBuilder()
.WithUrl(hubUrl)
.ConfigureLogging(logging =>
{
logging.AddConsole();
logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Information);
})
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions = JsonConvertersFactory.CreateDefaultJsonConverters(LoggerMotionDetection, LoggerMotionInfo, LoggerJsonVisitor);
})
.Build();
hubConnection.On<MotionDetection>("ReceiveMotionDetection", ReceiveMessage);
hubConnection.Closed += CloseHandler;
Logger.LogInformation("Starting HubConnection");
await hubConnection.StartAsync();
Logger.LogInformation("Index Razor Page initialised, listening on signalR hub url => " + hubUrl.ToString());
}
catch (Exception e)
{
Logger.LogError(e, "Encountered exception => " + e);
}
}
}
protected override async Task OnInitializedAsync()
{
await Task.CompletedTask;
}
#endregion
#region signalR
/// <summary>Log signalR connection closing</summary>
/// <param name="exception">
/// If an exception occurred while closing then this argument describes the exception
/// If the signaR connection was closed intentionally by client or server, then this
/// argument is null
/// </param>
private Task CloseHandler(Exception exception)
{
if (exception == null)
{
Logger.LogInformation("signalR client connection closed");
}
else
{
Logger.LogInformation($"signalR client closed due to error => {exception.Message}");
}
return Task.CompletedTask;
}
/// <summary>
/// Add motion detection notification to repository
/// </summary>
/// <param name="message">Motion detection received via signalR</param>
private void ReceiveMessage(MotionDetection message)
{
try
{
Logger.LogInformation("Motion detection message received");
Repository.AddItem(message);
StateHasChanged();
}
catch (Exception ex)
{
Logger.LogError(ex, "An exception was encountered => " + ex.ToString());
}
}
#endregion
}
}
signalR 服务器集线器
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
namespace WebApp.Realtime.SignalR
{
/// <summary>
/// This represents endpoints available on the server, available for the
/// clients to call
/// </summary>
public class MotionHub : Hub<IMotion>
{
private bool _disposed = false;
public ILogger<MotionHub> Logger { get; set; }
public MotionHub(ILogger<MotionHub> logger) : base()
{
Logger = logger;
}
public override async Task OnConnectedAsync()
{
Logger.LogInformation($"OnConnectedAsync => Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name}");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
if (exception != null)
{
Logger.LogInformation($"OnDisconnectedAsync => Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name} : Exception={exception.Message}");
}
else
{
Logger.LogInformation($"OnDisconnectedAsync => Connection ID={Context.ConnectionId} : User={Context.User.Identity.Name}");
}
await base.OnDisconnectedAsync(exception);
}
// Protected implementation of Dispose pattern.
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
// Call base class implementation.
base.Dispose(disposing);
}
}
}
【问题讨论】:
-
尝试使用 try-Catch-Finally 语法来关闭/释放连接。像这样:
try{ await hubConnection.StopAsync(); } catch (Exception exception) { Logger.LogInformation(exception.Message);} finally { await hubConnection.DisposeAsync();}. -
我想可能与从 Dispose 中调用 CloseAsync 有关,到达时状态已设置为已释放,因此可能通过异常,检查 [Blazor wasm SignalR 未关闭连接](github.com/mono/mono/issues/18628) 和 [在 .NET Core 中处理 SignalR HubConnection 的最佳实践](github.com/dotnet/aspnetcore/issues/13082)。
-
谢谢@zhi-liv 好建议。尝试将异常处理块放在
hubConnection.StopAsync()周围。不会抛出异常。看起来它正在阻止StopAsync。在 github 讨论中询问 question 并在Dispose方法中将DisposeAsync().GetAwaiter().GetResult();更改为_ = DisposeAsync()后,它可以工作。不知道为什么?等待 gihub 上的后续答复。
标签: asp.net-core signalr blazor-server-side signalr.client asp.net-core-signalr