1
\$\begingroup\$

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 queue 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.

My idea to was to implement 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..
}

Any ideas for improvement? Any special cases I haven't thought of and handled in my code?

\$\endgroup\$
1
\$\begingroup\$

Database model

Let's simplify your database model first - which I find is a little bit strange.

The Connection doesn't need anything but those two properties:

public class Connection
{
    [Key]
    [DatabaseGenerationOptions.None]
    public string ConnectionId { get; set; }
    public bool Connected { get; set; } 
}

Everything else you can track with the Notification. I added a new SentOn property that is nullable and empty if a notification couldn't be sent.

public abstract class Notification
{   
    public int Id { get; set; }
    public int ConnectionId { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime? SentOn { get; set; }
}

I think you'll know how to use it. Save notification as you do and later get only those where the SentOn property is null.

DbContext

public MyHub(DbContext db)
{
  this._db = db;
}

Using the DbContext in this way is usually a bad idea. It should be disposed as soon as possible.

Besides this code cannot work. You pass it a DbContext but later in code you use the Connections property which doesnt't belong to this object but to the UserContext.


  var openConnections = _db.Connections.Where(x => x.Connected);
  var closedConnections = _db.Connections.Where(x => !x.Connected);

This means two queries.

It'd be better to just loop over all connections and put an if inside the loop:

foreach (var connection in _db.Connections)
{
    if(connection.Connected)
    {
    _hubContext.Clients.Client(connection.ConnectionID).handleNotification(notification);
    }
    else
    {
        connection.MissedNotifications.add(notification);
    }
}
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy