Blazor Server迁移后SignalR多用户连接异常问题求助
Blazor Server自定义SignalR连接仅最后加载用户接收事件的问题
问题场景
原WASM项目通过JavaScript实现的SignalR实时功能正常,迁移到Blazor Server后出现异常:第一个用户打开页面后SignalR事件触发正常,第二个用户打开页面后,第一个用户的SignalR事件停止触发,仅最后加载页面的用户能收到JS调用的.NET方法。
相关代码
JavaScript端SignalR实现
$("#app").aeListConnection({ access_token: token, application: application, url: url, onconnect: function (data) { DotNet.invokeMethodAsync(WebAssemblyName, "ConnectIdFromSignalRMultipleConnection", data.id); if (window.location.href.toLowerCase().indexOf("login") > -1) { DotNet.invokeMethodAsync(WebAssemblyName,"ConnectionReloaded"); } }, onpreview: function (data) { functionToExecute = createFunction("DataFromSignalR") console.log("preview and exec " + functionToExecute); DotNet.invokeMethodAsync(WebAssemblyName,functionToExecute, data); }, onfeedback: function (data) { functionToExecute = createFunction("FeedbackFromSignalR") console.log("feedback and exec " + functionToExecute + " Status: " + data.Status + " UserId: " + data.UserId); DotNet.invokeMethodAsync(WebAssemblyName,functionToExecute, data.Status); }, onupdate: function (data, msg) { functionToExecute = createFunction("DataFromSignalR") console.log("update and exec " + functionToExecute + " " + msg); document.getElementById("CurrentBid").innerHTML = data.CurrentBid; DotNet.invokeMethodAsync(WebAssemblyName,functionToExecute, data); } });
Blazor端接收代码
[JSInvokable] public static void DataFromSignalRLotDetails(dynamic value) { BridgeFunctionDataFromSignalR.Invoke(value); }
private async Task DataFromSignalRAsync(dynamic value) { // 业务逻辑代码 }
服务注册代码(Program.cs)
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddHttpClient(); // setting client host environment builder.Services.AddSingleton<IHostEnvironment>( new HostingEnvironment() { EnvironmentName = builder.Environment.EnvironmentName }); builder.Logging.AddConfiguration( builder.Configuration.GetSection("Logging") ); builder.Services.AddScoped<ILotService, LotService>(); builder.Services.AddScoped<IAuthService, AuthService>(); builder.Services.AddScoped<IAuctionService, AuctionService>(); builder.Services.AddScoped<ICategoryService, CategoryService>(); builder.Services.AddScoped<IMemberService, MemberService>(); builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IUseCasesService, UseCasesService>(); builder.Services.AddScoped<IAuthenticationService, AuthenticationService>(); builder.Services.AddScoped<IContactService, ContactService>(); builder.Services.AddScoped<ICountryService, CountryService>(); builder.Services.AddScoped<ILanguageService, LanguageService>(); builder.Services.AddScoped<IPaymentService, PaymentService>(); builder.Services.AddScoped<IInvoiceService, InvoiceService>(); builder.Services.AddScoped<IUmbracoService, UmbracoService>(); builder.Services.AddBlazoredSessionStorage(); builder.Services.AddBlazoredLocalStorage(); builder.Services.AddOptions(); builder.Services.AddAuthorizationCore(); builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>(); builder.Services.AddIntersectionObserver(); builder.Services.AddSingleton<PageHistoryState>(); builder.Services.AddSingleton<ManageSignalRState>(); builder.Services.AddSingleton<CategorySingleton>(); builder.Services.AddSingleton<AuctionSingleton>(); builder.Services.AddSingleton<UmbracoSingleton>(); builder.Services.AddHeadElementHelper(); builder.Services.AddSocialMediaSharing(); builder.Services.AddLocalization(); var supportedCultures = new[] { "en-US", "id-ID" }; var localizationOptions = new RequestLocalizationOptions() .SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); var app = builder.Build(); app.UseRequestLocalization(localizationOptions); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); app.Run();
问题根源
- 静态成员全局共享:
DataFromSignalRLotDetails是静态方法,BridgeFunctionDataFromSignalR是静态委托。Blazor Server中每个用户会话对应独立的组件实例,静态成员会被所有会话共享,后续用户的组件会覆盖委托引用,导致所有JS调用都指向最后一个用户的实例。 - JS调用未绑定实例:直接调用静态.NET方法,没有关联到具体用户的组件实例,无法区分不同用户的会话上下文。
解决方案
方案一:绑定JS调用到组件实例
将JS回调与当前组件实例绑定,避免全局静态成员的共享问题。
1. 修改Blazor组件代码
@inject IJSRuntime JS @implements IDisposable @code { private DotNetObjectReference<YourComponentType>? _dotNetRef; private string token = "your-token"; private string application = "your-app"; private string url = "signalr-url"; private const string WebAssemblyName = "YourAssemblyName"; protected override void OnInitialized() { _dotNetRef = DotNetObjectReference.Create(this); // 调用JS方法传递组件实例引用 _ = JS.InvokeVoidAsync("registerSignalRCallbacks", _dotNetRef, WebAssemblyName, token, application, url); } [JSInvokable] public void ConnectIdFromSignalRMultipleConnection(string connectionId) { // 处理连接ID逻辑 } [JSInvokable] public void ConnectionReloaded() { // 处理连接重载逻辑 } [JSInvokable] public async Task DataFromSignalRLotDetails(dynamic value) { await DataFromSignalRAsync(value); } [JSInvokable] public async Task FeedbackFromSignalR(string status) { // 处理反馈逻辑 } private async Task DataFromSignalRAsync(dynamic value) { // 原业务逻辑代码 } public void Dispose() { _dotNetRef?.Dispose(); } }
2. 重构JavaScript代码
window.registerSignalRCallbacks = function(dotNetRef, webAssemblyName, token, application, url) { $("#app").aeListConnection({ access_token: token, application: application, url: url, onconnect: function (data) { dotNetRef.invokeMethodAsync("ConnectIdFromSignalRMultipleConnection", data.id); if (window.location.href.toLowerCase().indexOf("login") > -1) { dotNetRef.invokeMethodAsync("ConnectionReloaded"); } }, onpreview: function (data) { dotNetRef.invokeMethodAsync("DataFromSignalRLotDetails", data); }, onfeedback: function (data) { dotNetRef.invokeMethodAsync("FeedbackFromSignalR", data.Status); }, onupdate: function (data, msg) { document.getElementById("CurrentBid").innerHTML = data.CurrentBid; dotNetRef.invokeMethodAsync("DataFromSignalRLotDetails", data); } }); };
方案二:Scoped服务管理SignalR连接
将SignalR连接逻辑封装为Scoped服务(每个用户会话一个实例),通过事件通知组件更新。
1. 创建Scoped服务
public class SignalRConnectionService : IDisposable { private readonly IJSRuntime _js; public event EventHandler<dynamic>? OnDataReceived; public event EventHandler<string>? OnFeedbackReceived; public event EventHandler<string>? OnConnected; public SignalRConnectionService(IJSRuntime js) { _js = js; } public async Task InitializeConnection(string token, string application, string url) { var dotNetRef = DotNetObjectReference.Create(this); await _js.InvokeVoidAsync("initializeSignalRConnection", dotNetRef, token, application, url); } [JSInvokable] public void OnConnect(string connectionId) { OnConnected?.Invoke(this, connectionId); } [JSInvokable] public void OnDataFromSignalR(dynamic data) { OnDataReceived?.Invoke(this, data); } [JSInvokable] public void OnFeedback(string status) { OnFeedbackReceived?.Invoke(this, status); } public void Dispose() { _js.InvokeVoidAsync("disposeSignalRConnection"); } }
2. 注册Scoped服务
在Program.cs中添加:
builder.Services.AddScoped<SignalRConnectionService>();
3. 组件中使用服务
@inject SignalRConnectionService SignalRService @implements IDisposable @code { private string token = "your-token"; private string application = "your-app"; private string url = "signalr-url"; protected override void OnInitialized() { SignalRService.OnDataReceived += HandleDataReceived; _ = SignalRService.InitializeConnection(token, application, url); } private async void HandleDataReceived(object? sender, dynamic value) { await DataFromSignalRAsync(value); } private async Task DataFromSignalRAsync(dynamic value) { // 原业务逻辑代码 } public void Dispose() { SignalRService.OnDataReceived -= HandleDataReceived; } }
4. 对应JavaScript代码
window.initializeSignalRConnection = function(dotNetRef, token, application, url) { window.currentSignalRConnection = $("#app").aeListConnection({ access_token: token, application: application, url: url, onconnect: function (data) { dotNetRef.invokeMethodAsync("OnConnect", data.id); if (window.location.href.toLowerCase().indexOf("login") > -1) { dotNetRef.invokeMethodAsync("ConnectionReloaded"); } }, onpreview: function (data) { dotNetRef.invokeMethodAsync("OnDataFromSignalR", data); }, onfeedback: function (data) { dotNetRef.invokeMethodAsync("OnFeedback", data.Status); }, onupdate: function (data, msg) { document.getElementById("CurrentBid").innerHTML = data.CurrentBid; dotNetRef.invokeMethodAsync("OnDataFromSignalR", data); } }); }; window.disposeSignalRConnection = function() { if (window.currentSignalRConnection) { // 调用aeListConnection的销毁方法(根据实际API调整) window.currentSignalRConnection.dispose?.(); delete window.currentSignalRConnection; } };
内容的提问来源于stack exchange,提问作者Jesus Fortuna
相关产品推荐
相关产品推荐

