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

.NET 8中如何通过SignalR从ASP.NET Core后台服务向Blazor WASM特定用户发实时通知

SignalR向特定用户发送实时通知的实现问题

我是SignalR新手,首次学习实践该技术。在.NET 8环境下,我尝试通过ASP.NET Core Web API的BackgroundService,借助SignalR向Blazor WASM应用中的特定用户发送实时通知。当前设置能正常向所有客户端发送通知,但想改成定向发送,不清楚怎么在BackgroundService里获取目标用户ID,也不确定现有思路是否正确。


客户端代码

private HubConnection? _hubConnection;
private readonly List<string> _notifications = new();

[Inject] IAccessTokenProvider TokenProvider { get; set; }

[Inject] IConfiguration Configuration { get; set; }

[CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }

protected override async Task OnInitializedAsync()
{
    var authState = await authenticationStateTask;
    var user = authState.User;

    if (user.Identity.IsAuthenticated)
    {
        _hubConnection = new HubConnectionBuilder()
                        .WithUrl($"{Configuration.GetValue<string>("ApiBaseAddress")}notifications",
                        options =>
                        {
                            options.AccessTokenProvider = async () =>
                            {
                                var accessTokenResult = await TokenProvider.RequestAccessToken();
                                accessTokenResult.TryGetToken(out var token);
                                return token.Value;
                            };
                        })
                        .Build();

        _hubConnection.On<string>("ReceiveNotification", notification =>
        {
            _notifications.Add(notification);
            InvokeAsync(StateHasChanged);
        });

        await _hubConnection.StartAsync();
    }
}

服务端代码

Hub定义

[Authorize]
public class NotificationsHub : Hub<INotificationClient>
{
}

public interface INotificationClient
{
    Task ReceiveNotification(string message);
}

Program.cs配置

...
builder.Services.AddSignalR();
...
app.MapHub<NotificationsHub>("notifications");

现有BackgroundService代码

public class InventoryNotifier(
    IHubContext<NotificationsHub, INotificationClient> hubContext,
    ILogger<InventoryNotifier> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

        while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
        {
            await hubContext.Clients
                .All
                .ReceiveNotification($"InventoryNotifier is starting... {DateTime.Now}");
        }

        stoppingToken.Register(() => logger.LogWarning($"{nameof(InventoryNotifier)} is stopping due to host shut down."));
    }
}

当前问题

现在使用Clients.All可以实现群发通知,但要改为Clients.User(userId)定向发送时,不清楚如何在BackgroundService中获取目标用户ID。尝试使用IUserIdProvider.GetUserId()方法,但该方法需要HubConnectionContext,而BackgroundService中无法获取这个上下文。

我曾尝试的错误逻辑:

public class InventoryNotifier(
    IHubContext<NotificationsHub, INotificationClient> hubContext,
    IUserIdProvider userIdProvider,
    ILogger<InventoryNotifier> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
        
        while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
        {
            await hubContext.Clients
                .User(userIdProvider.GetUserId()) // 此处无法获取HubConnectionContext,逻辑错误
                //.All
                .ReceiveNotification($"InventoryNotifier is starting... {DateTime.Now}");
        }

        stoppingToken.Register(() => logger.LogWarning($"{nameof(InventoryNotifier)} is stopping due to host shut down."));
    }
}

解决方案

核心思路纠正

IUserIdProvider.GetUserId()是在SignalR连接建立时,从当前活跃的连接上下文(HubConnectionContext)中提取用户ID的方法,它不能直接在BackgroundService中调用——因为BackgroundService是后台运行的服务,没有绑定任何活跃的SignalR连接上下文。

正确的做法是:直接使用业务场景中已知的目标用户ID,或者从数据库、业务服务等渠道动态获取用户ID,再通过Clients.User(targetUserId)发送定向通知。


具体实现步骤

1. 确认SignalR能正确识别用户ID

默认情况下,SignalR会从认证用户的ClaimTypes.NameIdentifier Claim中获取用户ID。你可以在Hub的OnConnectedAsync方法中验证当前连接的用户ID是否正确:

[Authorize]
public class NotificationsHub : Hub<INotificationClient>
{
    public override async Task OnConnectedAsync()
    {
        // 获取当前连接的用户ID,可用于调试验证
        var currentUserId = Context.UserIdentifier;
        await base.OnConnectedAsync();
    }
}

如果你的用户ID来自其他Claim(比如邮箱),可以自定义IUserIdProvider:

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public string? GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
    }
}

然后在Program.cs中注册这个自定义提供器:

builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

2. 在BackgroundService中使用目标用户ID发送通知

根据业务场景,目标用户ID的获取方式分两种:

方式一:固定目标用户ID

如果是给特定固定用户发通知,直接在BackgroundService中定义目标ID即可:

public class InventoryNotifier(
    IHubContext<NotificationsHub, INotificationClient> hubContext,
    ILogger<InventoryNotifier> logger) : BackgroundService
{
    // 实际业务中可从配置、数据库等地方获取
    private readonly string _targetUserId = "user_12345";

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
        
        while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
        {
            await hubContext.Clients
                .User(_targetUserId)
                .ReceiveNotification($"InventoryNotifier is starting... {DateTime.Now}");
        }

        stoppingToken.Register(() => logger.LogWarning($"{nameof(InventoryNotifier)} is stopping due to host shut down."));
    }
}
方式二:动态获取目标用户ID

如果需要根据业务逻辑动态获取用户ID(比如监控到某个用户的库存不足,就通知该用户),可以注入对应的业务服务或仓储来获取:

// 假设这是你的用户仓储接口
public interface IUserRepository
{
    Task<string?> GetTargetInventoryUserIdAsync(CancellationToken cancellationToken);
}

public class InventoryNotifier(
    IHubContext<NotificationsHub, INotificationClient> hubContext,
    IUserRepository userRepository,
    ILogger<InventoryNotifier> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
        
        while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
        {
            // 从业务仓储获取需要通知的用户ID
            var targetUserId = await userRepository.GetTargetInventoryUserIdAsync(stoppingToken);
            if (!string.IsNullOrEmpty(targetUserId))
            {
                await hubContext.Clients
                    .User(targetUserId)
                    .ReceiveNotification($"InventoryNotifier is starting... {DateTime.Now}");
            }
        }

        stoppingToken.Register(() => logger.LogWarning($"{nameof(InventoryNotifier)} is stopping due to host shut down."));
    }
}

内容的提问来源于stack exchange,提问作者fingers10

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 05:34:58