-
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 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:
[Authorize]
public class NotificationsHub : Hub<INotificationClient>
{
}
public interface INotificationClient
{
Task ReceiveNotification(string message);
}
...
builder.Services.AddSignalR();
...
app.MapHub<NotificationsHub>("notifications");
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 await hubContext.Clients
.All // <--- This sends for All Client.
.ReceiveNotification($"InventoryNotifier is starting... {DateTime.Now}"); When i tried to send to specific How can i get the user id inside background service? After some research I noticed that I can use 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 The final idea i had in my mind is to have something like the one below. 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? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 10 replies
-
This is the question. How does the background service running on a webserver handling many users know which user it should send updates to? Is there any operation it can query with the associated user context? |
Beta Was this translation helpful? Give feedback.
-
@davidfowl Thank you for your time and support. Thank you for your patience. Here is my implementation. 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:
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);
}
}
...
builder.Services.AddSignalR();
builder.Services.AddHostedService<InventoryNotifierService>();
builder.Services.AddSingleton<ConnectedUsers>();
...
app.MapHub<NotificationsHub>("notifications");
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."));
}
} |
Beta Was this translation helpful? Give feedback.
@davidfowl Thank you for your time and support. Thank you for your patience. Here is my implementation.
Client Side: