如何使用 .NET 8 中的 SignalR 从 ASP.NET Core 后台服务向 Blazor wasm 客户端中的特定用户发送实时通知

问题描述 投票:0回答:1

我是 SignalR 新手,第一次学习和练习。我正在尝试使用 .NET 8 中的 SignalR,按照以下代码从 ASP.NET Core Web API 中的

User
向我的 Blazor WASM 应用程序中的特定
BackgroundService
发送实时通知。

客户端:

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."));
    }
}

上述设置运行良好,我在 Blazor wasm 客户端应用程序中收到通知。不过,我正在从后台服务向

Clients.All
发送消息。

await hubContext.Clients
                .All // <--- This sends for All Client.
                .ReceiveNotification($"InventoryNotifier is starting... {DateTime.Now}");

当我尝试发送到特定的

User
时,我注意到
Clients.User()
存在并且需要一个
userId

如何在后台服务中获取用户ID?经过一番研究后,我注意到我可以使用

IUserIdProvider
并从中调用
GetUserId()
方法,如下所示。

namespace Microsoft.AspNetCore.SignalR;

/// <summary>
/// A provider abstraction for configuring the "User ID" for a connection.
/// </summary>
/// <remarks><see cref="IUserIdProvider"/> is used by <see cref="IHubClients{T}.User(string)"/> to invoke connections associated with a user.</remarks>
public interface IUserIdProvider
{
    /// <summary>
    /// Gets the user ID for the specified connection.
    /// </summary>
    /// <param name="connection">The connection to get the user ID for.</param>
    /// <returns>The user ID for the specified connection.</returns>
    string? GetUserId(HubConnectionContext connection);
}

但这又需要

HubConnectionContext
。现在我不知道如何将
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()) // <---- Not sure how to get HubConnectionContext here
                //.All
                .ReceiveNotification($"InventoryNotifier is starting... {DateTime.Now}");
        }

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

请问有人可以帮助我吗?这是正确的方法吗?还是我让事情变得复杂了?

.net asp.net-core blazor signalr blazor-webassembly
1个回答
0
投票

在问题的评论部分获得 @BrianParker 的指导并遵循他的示例存储库 github.com/BrianLParker/Blazor8WithRolesAndSignalR 后,我能够实现这一目标,如下所示。

客户端

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

public class NotificationsHub(ConnectedUsers connectedUsers,
    ILogger<NotificationsHub> logger) : Hub<INotificationClient>
{
    public override async Task OnConnectedAsync()
    {
        //await Clients.Client(Context.ConnectionId).ReceiveNotification($"Connected {Context.User?.Identity?.Name}");

        var userToNotify = new
        {
            UserId = Guid.Parse(Context.User!.FindFirst("sub")!.Value),
            BranchId = Guid.Parse(Context.User!.FindFirst("branchId")!.Value)
        };

        await Groups.AddToGroupAsync(Context.ConnectionId, userToNotify.BranchId.ToString());

        connectedUsers.AddUser(userToNotify.UserId, userToNotify.BranchId);

        logger.LogInformation($"User {userToNotify.UserId} connected to branch {userToNotify.BranchId}");

        await base.OnConnectedAsync();
    }

    public override Task OnDisconnectedAsync(Exception? exception)
    {
        var userToNotify = new
        {
            UserId = Guid.Parse(Context.User!.FindFirst("sub")!.Value),
            BranchId = Guid.Parse(Context.User!.FindFirst("branchId")!.Value)
        };

        logger.LogInformation($"User {userToNotify.UserId} disconnected from branch {userToNotify.BranchId}");

        connectedUsers.RemoveUser(userToNotify.UserId);

        if (exception is not null) 
        {
            logger.LogError(exception, "An error occurred during disconnection.");
        }

        return base.OnDisconnectedAsync(exception);
    }
}

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

public class ConnectedUsers 
{
    public Dictionary<Guid,Guid> Users { get; private set; } = [];

    public void AddUser(Guid userId, Guid branchId)
    {
        Users.TryAdd(userId, branchId);
    }

    public void RemoveUser(Guid userId)
    {
        Users.Remove(userId);
    }
}

Program.cs

...
builder.Services.AddSignalR();
builder.Services.AddHostedService<InventoryNotifierService>();
builder.Services.AddSingleton<ConnectedUsers>();
...
app.MapHub<NotificationsHub>("notifications");

BackgroundService

public class InventoryNotifierService(
    IHubContext<NotificationsHub, INotificationClient> hubContext,
    IServiceProvider serviceProvider,
    ConnectedUsers connectedUsers,
    ILogger<InventoryNotifierService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
            using var scope = serviceProvider.CreateScope();
            var isolatedReadContext = scope.ServiceProvider.GetRequiredService<AnyBillsBaseIsolatedReadContext>();

            while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
            {
                var branches = connectedUsers.Users.Values.Distinct().ToList();

                var items = await isolatedReadContext
                        .Items
                        .Where(i => branches.Contains(i.BranchId) && i.Product && i.Quantity < 5)
                        .GroupBy(i => i.BranchId)
                        .ToListAsync();

                foreach (var branchItems in items)
                {
                    foreach (var item in branchItems)
                    {
                        await hubContext.Clients
                            .Group(branchItems.Key.ToString())
                            .ReceiveNotification($"You have {item.Name} {item.Quantity} in your inventory.");
                    }
                }
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error while reading branch items for sending notifications.");
        }

        stoppingToken.Register(() => logger.LogWarning($"{nameof(InventoryNotifierService)} is stopping due to host shut down."));
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.