I'm working on a .net 6.0 project using SignalR to send notifications to users. I wanna store the notifications from users who were disconnected and re-send the notifications to these reconnected users. I'd like to use memory storage for all this. I can't find information and documentation about that.
This is my Hub:
public class NotificationHub : Hub<INotificationHub>
{
private readonly IUserConnectionManager _userConnectionManager;
public NotificationHub(IUserConnectionManager userConnectionManager)
{
_userConnectionManager = userConnectionManager;
}
private readonly Dictionary<string, string> _userConnections = new Dictionary<string, string>();
public override async Task OnConnectedAsync()
{
var token = Context.GetHttpContext().Request.Headers["access_token"];
if (!string.IsNullOrEmpty(token))
{
bool successfulConnection = UserConnection(token);
if (successfulConnection)
{
var response = new ServerResponse
{
status = "Accepted",
};
await Clients.Client(Context.ConnectionId).ConnectionResponse(response);
}
}
else
{
var response = new ServerResponse
{
status = "Error",
message = "Token no proporcionado"
};
await Clients.Client(Context.ConnectionId).ConnectionResponse(response);
Context.Abort();
}
}
private bool UserConnection(string token)
{
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(token) as JwtSecurityToken;
if (jsonToken != null)
{
string userId = jsonToken.Claims.FirstOrDefault(claim => claim.Type == "email")?.Value;
if (!string.IsNullOrEmpty(userId))
{
GetConnectionId(userId);
}
var response = new ServerResponse
{
status = "Accepted",
};
return true;
}
return false;
}
public string GetConnectionId(string userId)
{
_userConnectionManager.KeppUserConnection(userId, Context.ConnectionId);
return Context.ConnectionId;
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var connectionId = Context.ConnectionId;
_userConnectionManager.RemoveUserConnection(connectionId);
await base.OnDisconnectedAsync(exception);
}
public async Task MessageCheck(NotificationRequestModel request)
{
//meter validaciones
switch (request.Target.Device)
{
case "Admin":
if (request.Target.User.Contains("*"))
{
SendNotificationToAllUsers(request.Notification);
}
else
{
SendNotificationToSpecificUser(request.Notification, request.Target.User);
}
break;
}
}
public async Task SendNotificationToAllUsers(NotificationModel message)
{
await Clients.All.SendNotificationToAllUsers(message);
}
public async Task SendNotificationToSpecificUser(NotificationModel message, List<string> target)
{
foreach (var userId in target)
{
var connectionIds = _userConnectionManager.GetUserConnections(userId);
foreach (var connectionId in connectionIds)
{
await Clients.Client(connectionId).SendNotification(message);
}
}
}
You can implement this requirement by following code.
InMemoryNotificationStore.cs
using ASPNETCORE8.Models;
namespace ASPNETCORE8.IServices
{
public class InMemoryNotificationStore : INotificationStore
{
private readonly Dictionary<string, List<NotificationModel>> _notifications = new();
public void StoreNotification(string userId, NotificationModel notification)
{
if (!_notifications.ContainsKey(userId))
{
_notifications[userId] = new List<NotificationModel>();
}
_notifications[userId].Add(notification);
}
public List<NotificationModel> RetrieveNotifications(string userId)
{
if (_notifications.ContainsKey(userId))
{
return _notifications[userId];
}
return new List<NotificationModel>();
}
public void ClearNotifications(string userId)
{
if (_notifications.ContainsKey(userId))
{
_notifications.Remove(userId);
}
}
}
}
INotificationStore.cs
using ASPNETCORE8.Models;
namespace ASPNETCORE8.IServices
{
public interface INotificationStore
{
void StoreNotification(string userId, NotificationModel notification);
List<NotificationModel> RetrieveNotifications(string userId);
void ClearNotifications(string userId);
}
}
And this is my NotificationHub.cs
using ASPNETCORE8.IServices;
using ASPNETCORE8.Models;
using Microsoft.AspNetCore.SignalR;
using System.Diagnostics;
namespace ASPNETCORE8.Hubs
{
public class NotificationHub : Hub<INotificationHub>
{
private readonly IUserConnectionManager _userConnectionManager;
private readonly INotificationStore _notificationStore;
public NotificationHub(IUserConnectionManager userConnectionManager, INotificationStore notificationStore)
{
_userConnectionManager = userConnectionManager;
_notificationStore = notificationStore;
}
public override async Task OnConnectedAsync()
{
var token = Context.GetHttpContext().Request.Headers["access_token"];
// the logic of checking your token
// checking the notifications once connected
var userId = "testid";
// get the messages from memory
var notifications = _notificationStore.RetrieveNotifications(userId);
foreach (var notification in notifications)
{
await Clients.Client(Context.ConnectionId).SendNotification(notification);
}
_notificationStore.ClearNotifications(userId);
await base.OnConnectedAsync();
}
public async Task SendNotificationToSpecificUser(NotificationModel message, List<string> target)
{
foreach (var userId in target)
{
var connectionIds = _userConnectionManager.GetConnectionsByUserid(userId);
// check the user is online or not
if (connectionIds.Count > 0)
{
foreach (var connectionId in connectionIds)
{
await Clients.Client(connectionId).SendNotification(message);
}
}
else
{
// if user is offline, then save the notification messages
_notificationStore.StoreNotification(userId, message);
}
}
}
}
}
Other files, INotificationHub.cs
using ASPNETCORE8.Models;
namespace ASPNETCORE8.IServices
{
public interface INotificationHub
{
Task SendNotification(NotificationModel notification);
Task ConnectionResponse(ServerResponse response);
}
}
IUserConnectionManager.cs
namespace ASPNETCORE8.IServices
{
public interface IUserConnectionManager
{
void AddConnection(string userId, string connectionId);
void RemoveConnection(string userId, string connectionId);
List<string> GetConnectionsByUserid(string userId);
Dictionary<string, List<string>> GetAllConnections();
}
}
NotificationModel.cs
namespace ASPNETCORE8.Models
{
public class NotificationModel
{
public string? Title { get; set; }
public string? Message { get; set; }
public DateTime Date { get; set; }
public NotificationModel()
{
}
public NotificationModel(string title, string message, DateTime date)
{
Title = title;
Message = message;
Date = date;
}
}
}
ServerResponse.cs
namespace ASPNETCORE8.Models
{
public class ServerResponse
{
public string? Status { get; set; }
public string? Message { get; set; }
}
}
UserConnectionManager.cs
using ASPNETCORE8.IServices;
using System.Collections.Concurrent;
namespace ASPNETCORE8.Services
{
public class UserConnectionManager : IUserConnectionManager
{
private readonly ConcurrentDictionary<string, List<string>> _connections = new ConcurrentDictionary<string, List<string>>();
public void AddConnection(string userId, string connectionId)
{
_connections.AddOrUpdate(userId, new List<string> { connectionId }, (key, oldValue) =>
{
oldValue.Add(connectionId);
return oldValue;
});
}
public void RemoveConnection(string userId, string connectionId)
{
if (_connections.TryGetValue(userId, out var connections))
{
connections.Remove(connectionId);
if (connections.Count == 0)
{
_connections.TryRemove(userId, out _);
}
}
}
public List<string> GetConnectionsByUserid(string userId)
{
if (_connections.TryGetValue(userId, out var connections))
{
return connections;
}
return new List<string>();
}
public Dictionary<string, List<string>> GetAllConnections()
{
return _connections.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
}
}
And the settings in Program.cs.
...
builder.Services.AddSignalR();
builder.Services.AddSingleton<INotificationStore, InMemoryNotificationStore>()
...
var app = builder.Build();