0
votes

I have Organizations that login to an asp.net website and when each member logs in I add their ConnectionId and OrganizationId to a static ConcurrentDictionary named OrganizationMembers in a SignalR Hub OnConnected override. I do this so I can send data to a page callback function that contains data relative only to that Organization (SignalR Group). The OrganizationId will represent the SignalR Group that I send data to.

Also in the OnConnnected method, I add the OrganizationId and OrganizationId.ToString() to another ConcurrentDictionary that represents unique Organization Ids. I'll explain later why I store the OrganizationId.ToString(). I store unique Organization Ids so that in a background Task that calls a method and sleeps over and over, I can enumerate the unique Organization Ids and only send each Organization (SignalR Group) data relevant to it.

In the OnDisconnected Hub override, after I remove a Connection, I want to check the OrganizationId values in the OrganizationMembers ConcurrentDictionary to see if that was the last Member with that OrganizationId to disconnect and if so remove it from the UniqueOrganizations dictionary. I know the dictionary Values.Contains() is O(n) so I'd really like to avoid this.

This is so when the task enumerates the UniqueOrganizations, there won't be Organizations (SignalR Groups) that I'm attempting to send data to needlessly in the case that say for example 5 Members from the same Organization log in but all later close their browser, I won't be attempting to send that Group data via the SignalR callback.

Admittedly I don't know the internal workings of the SignalR Hub so it may be the case that it won't matter if I try to send data needlessly to Members of Organizations (SignalR Groups) that have all disconnected.

Is this over-thinking the SignalR Hub? Should I not worry about determining if the last Organization (SignalR Group) Member has disconnected from the Hub and not remove the OrganizationId from the UniqueOrganizations?

If this is OK to do, how can I avoid the dictionary Values.Contains() since it's O(n)?

// the key is the ConnectionId of an Organization Member
// the value is the OrganizationId of the Member
protected static ConcurrentDictionary<string, int> _organizationMembers;
public static ConcurrentDictionary<string, int> OrganizationMembers {
    get {
        if(_organizationMembers == null) {
            _organizationMembers = new ConcurrentDictionary<string, int>();
        }

        return _organizationMembers;
    }
}

// the key is the OrganizationId to send the specific data to
// the value is the OrganizationId converted to string
protected static ConcurrentDictionary<int, string> _uniqueOrganizations;
public static ConcurrentDictionary<int, string> UniqueOrganizations {
    get {
        if(_uniqueOrganizations == null) {
            _uniqueOrganizations = new ConcurrentDictionary<int, string>();
        }

        return _uniqueOrganizations;
    }
}


// Hub Code

public override Task OnConnected() {
    string connectionId = Context.ConnectionId;
    string organizationId = Context.Request.QueryString["organizationId"];
    int organizationIdValue = int.Parse(organizationId);

    OrganizationMembers.TryAdd(connectionId, organizationIdValue);
    UniqueOrganizations.TryAdd(organizationIdValue, organizationId);

    // the organizationId represents the SignalR group
    Groups.Add(connectionId, organizationId);
    return base.OnConnected();
}


public override Task OnDisconnected() {
    string organizationId = string.Empty;
    int organizationIdValue;
    string connectionId = Context.ConnectionId;

    OrganizationMembers.TryRemove(connectionId, out organizationIdValue);

    // if that happens to be the last Member connection to be removed
    // then remove the OrganizationId from the unique OrganizationIds
    // so it won't be a wasted enumeration and useless callback

    // I want to avoid this O(n) Contains()
    if(!OrganizationMembers.Values.Contains(organizationIdValue)) {
        UniqueOrganizations.TryRemove(organizationIdValue, out organizationId);
    }

    Groups.Remove(connectionId, organizationId);
    return base.OnDisconnected();
}


// Task code
foreach(int organizationIdValue in DataCache.UniqueOrganizations.Keys) {
// this is why I also stored the OrganizationId as a string so I wouldn't have to
// convert it to a string each time the dictionary is enumerated.
// I can just have the organizationId string ready to use as the SignalR Group.
    string organizationId = UniqueOrganizations[organizationIdValue];

    try {
        string organizationData = GetOrganizationData(organizationIdValue);
        _clients.Group(organizationId).sendToOrganizationData(organizationData);
    }
    catch(Exception ex) {
        _clients.Group(organizationId).sendToOrganizationError();
    }
}
1

1 Answers

1
votes

First of all, SignalR is pretty smart about not wasting resources when sending to groups without any subscriptions, so you should be fine sending to groups without any members as long as it's OK to waste a few cycles doing that.

If you don't have too many organizations you can have a ConcurrentDictionary<int,int> with all your organization ids as your keys and the number of connected members as your value. In OnConnected and OnDisconnected in could use Interlocked.Increment and Interlocked.Decrement respectively to keep track of the currently connected members. Then in your task could loop over the keys and skip any organization with zero connected members.

This new ConcurrentDictionary could replace _uniqueOrganizations if you don't mind calling key.ToString(CultureInfo.InvariantCulture) to get the group name.