.netpush-notificationsignalr-hubasp.net-core-signalr

Memory Storage for Notifications Service using SignalR


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

Solution

  • You can implement this requirement by following code.

    enter image description here

    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();