.NET 8中如何通过SignalR从ASP.NET Core后台服务向Blazor WASM特定用户发实时通知
我是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

