Saturday, September 24, 2016

Managing signalR notifications to synchronize client and server (c#)

1 comment

In my web application I want to load all data to client side from the server on power up. After that I want all communication be managed through Signalr - meaning that each update the server will send notification to all clients and they will ask for the updated data.

However, I don't know what to do when the SingalR connection is corrupted and then goes back. I don't want to load all the data all over again. What I want to do is to implement some sort of notifications management on the server side for each disconnected client and whenever the SignalR connection is made again - push to that specific client all the notifications that he has missed.

Our signalR listeners on client side are made on singleton listeners instead of short living controllers, that so we can prevent GET request on each view change and make the application be faster and more user friendly. Because of that approach, new notifications in the background also get handled and processed even when it isn't relevant to the current view the end user is on, like so:

// This service is initialized once only public class Service1 {         static inject = ['$rootScope']     array : Item[];      // This is a singleton!     public constructor ($rootScope){          // Get all items from the server         GetAllItemsFromServer();          // Listener for signalR updates         var listener = $rootScope.$on("ItemsNotificationFromServer", UpdateItems);          $rootScope.$on('destroy', {             // Stop the listener             listener();         })     }         // Getting all the items from the server on each controller creation     GetAllItemsFromServer(){         // Getting the items     }      // Handle the notification from the server     public UpdateItems(event, result) : void          //..     } } 

At the moment what happens for example is that when an end user refreshes the browser (F5) I can not know what SignalR notifications this client has missed during the connection problems and so I load all the data from the server all over again (it sucks).

In order to prevent it I thought of implementing something like this -

namespace MapUsersSample {     public class UserContext : DbContext     {         // All those are cleaned when server is powered up         public DbSet<Connection> Connections { get; set; }         public DbSet<Notification> Notifications {get; set;}     }      public class Connection     {         [Key]         [DatabaseGenerationOptions.None]         public string ConnectionID { get; set; }         public bool Connected { get; set; }          // I fill this when disconnected         public List<Notification> MissedNotifications {get; set;}          public Connection(string id)         {             this.ConnectionID = id;             this.Connected = true;             this.MissedNotifications = new List<Notification>();         }     }      public abstract class Notification()     {         public int Id {get; set;}         public DateTime CreationTime {get; set;}     }      .. // Many notifications implement this }  public class MyHub : Hub {     private readonly DbContext _db;     public class MyHub(DbContext db)     {         this._db = db;     }      // Adding a new connection or updating status to true     public override Task OnConnected()     {         var connection = GetConnection(Context.ConnectionId);          if (connection == null)             _db.Connections.Add(new Connection(Context.ConnectionId));         else              connection.Connected = true;          return base.OnConnected()     }      // Changing connection status to false     public override Task OnDisconnected()     {         var connection = GetConnection(Context.ConnectionId);          if (connection == null)         {             Log("Disconnect error: failed to find a connection with id : " + Context.ConnectionId);             return;         }         else {             connection.Connected = false;         }         return base.OnDisconnected();     }      public override Task OnReconnected()     {        var connection = GetConnection(Context.ConnectionId);          if (connection == null)         {             Log("Reconnect error - failed to find a connection with id : " + Context.ConnectionId);             return;         }         else {             connection.Connected = true;         }          // On reconnect, trying to send to the client all the notifications that he has missed         foreach (var notification in connection.MissedNotifications){             Clients.Client(connection.ConnectionID).handleNotification(notification);         }          return base.OnReconnected();     }      // This method is called from clients that receive a notification     public clientNotified(int connectionId, int notificationId)     {         // Getting the connection         var connection = GetConnection(connectionId);          if (connection == null){             Log("clientNotified error - failed to find a connection with id : " + Context.ConnectionId);             return;         }          // Getting the notification that the client was notified about         var notificationToRemove = _dbConnection.Notifications.FirstOrDefault(n => n.Id == notificationId);          if (notificationToRemove == null)         {             Log("clientNotified error - failed to find notification with id : " + notificationId);             return;         }          // Removing from the missed notifications         connection.MissedNotifications.Remove(notificationToRemove);     }      private Connection GetConnection(int connectionId)      {         return _db.Connections.find(connectionId);     }   }  // Notifications outside of the hub public class Broadcaster {     DbContext _db;     public Broadcaster(DbContext db)     {         _hubContext = GlobalHost.ConnectionManager.GetHubContext<MoveShapeHub>();         _dbConnection = db;     }      public void NotifyClients(Notification notification)     {         var openConnections = _db.Connections.Where(x => x.Connected);         var closedConnections = _db.Connections.Where(x => !x.Connected);          // Adding all notifications to be sent when those connections are back         foreach (var connection in closedConnections){             connection.MissedNotifications.add(notification);         }          // Notifying all open connections         foreach (var connection in openConnections){             _hubContext.Clients.Client(connection.ConnectionID).handleNotification(notification);         }     } }   client side java script:  handleNotification(notification){     hubProxy.Server.clientNotified(hub.connection.id, notification.Id)      // Keep handling the notification here.. } 

I haven't got to test it yet, but before I present this idea to my team, is this approach popular? haven't seen people taking this approach and I wondered why? Are there any risks here?

2 Answers

Answers 1

You should check if the data is actual. It can be Hash or datetime of last change.

When client reconnect you should send actual data hash or datetime of last change to the client.

for example

{ clients: '2016-05-05T09:05:05', orders: '2016-09-20T10:11:11' }  

And the client application will decide what data it needs to update.

On client you can save data to LocalStorage or SessionStorage.

Answers 2

At the moment what happens for example is that when an end user refreshes the browser (F5) I can not know what SignalR notifications this client has missed during the connection problems and so I load all the data from the server all over again (it sucks).

Pressing F5 to refresh browser is a hard reset, all existing SignalR connection would be lost. New connections would be made to get data. Connection problems occur in scenarios when SignalR notices problems with the http connection for e.g. due to temporary network issues. Browser refresh isn't a connection problem, it's an act of a user knowingly recreating a new connection.

So, your code of managing missed notifications would work only for signalR connection issues. I don't think it'll work for browser refresh, but then it's a new connection so you haven't missed anything.

If You Enjoyed This, Take 5 Seconds To Share It

1 comment:

  1. In case you are interested in generating cash from your visitors by popup advertisments - you can embed one of the most reputable companies - Propeller Ads.

    ReplyDelete