I'm new to SignalR and learning it and practicing it for the first time. I'm trying to send real time notification to specific User
in my Blazor WASM app from a BackgroundService
in ASP.NET Core Web API using SignalR in .NET 8 as per the following code.
Client-side:
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();
}
}
I'm sending the auth token along with the connection.
Server-side:
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."));
}
}
The above setup works fine and I receive notifications in my Blazor wasm client app. However I'm sending messages to Clients.All
from background service.
await hubContext.Clients
.All // <--- This sends for All Client.
.ReceiveNotification($"InventoryNotifier is starting... {DateTime.Now}");
When I tried to send to specific User
, I noticed that Clients.User()
exists and that needs an userId
.
How can I get the user id inside background service? After some research I noticed that I can use IUserIdProvider
and call GetUserId()
method from that as shown below.
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);
}
But again this requires HubConnectionContext
. And now I'm not sure how to get HubConnectionContext
inside BackgroundService
.
The final idea I had in my mind is to have something like this:
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."));
}
}
Please can anyone assist me on this? Is this the right way? Or am I complicating it?
After getting guidance from @BrianParker in the comments section of the question and following his sample repo github.com/BrianLParker/Blazor8WithRolesAndSignalR , I was able to achieve this as shown below.
Client Side:
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();
}
}
I'm sending the auth token along with the connection.
Server Side:
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."));
}
}