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

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

问题根源

  1. 静态成员全局共享:DataFromSignalRLotDetails是静态方法,BridgeFunctionDataFromSignalR是静态委托。Blazor Server中每个用户会话对应独立的组件实例,静态成员会被所有会话共享,后续用户的组件会覆盖委托引用,导致所有JS调用都指向最后一个用户的实例。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 18:05:17