Disconnect not firing sometimes in signalR - c#

I currently keep my users in a table called OnlineUsers. When a person connects or disconnects it adds the userid and his connectionid to the table, but for some reason (i believe when multiple browser windows are open) the Disconnect function does not fire sometimes, leaving users in the table and making them appear "online" when they really aren't. Has anyone ran into this problem before and what would be a good way to fix this issue?
UPDATE** (sorry about not putting code, I should have done it in the first place)
Here are my db functions to add and remove from the table:
public bool ConnectUser(Guid UserId, String ConnectionId)
{
if (!Ent.OnlineUsers.Any(x => x.UserId == UserId && x.ConnectionId == ConnectionId))
{
Ent.OnlineUsers.AddObject(new OnlineUser { UserId = UserId, ConnectionId = ConnectionId });
Ent.SaveChanges();
return true;
}
else
return false;
}
public void DisconnectUser(Guid UserId, String ConnectionId)
{
if (Ent.OnlineUsers.Any(x => x.UserId == UserId && x.ConnectionId == ConnectionId))
{
Ent.OnlineUsers.DeleteObject(Ent.OnlineUsers.First(x => x.UserId == UserId && x.ConnectionId == ConnectionId));
Ent.SaveChanges();
}
}
Here is my hub class connect and disconnect task:
public Task Disconnect()
{
disconnectUser();
return null;
}
public Task Reconnect(IEnumerable<string> connections)
{
connectUser();
return null;
}
public Task Connect()
{
connectUser();
return null;
}
private void connectUser()
{
if (onlineUserRepository.ConnectUser(MainProfile.UserId, Context.ConnectionId))
{
Groups.Add(Context.ConnectionId, Convert.ToString(MainProfile.ChatId));
}
}
private void disconnectUser()
{
onlineUserRepository.DisconnectUser(MainProfile.UserId, Context.ConnectionId);
Groups.Remove(Context.ConnectionId, Convert.ToString(MainProfile.ChatId));
}
I have checked that I am on the latest version of signalR (0.5.3) and this seems to happen when I have multiple browser windows open and I close them all at once, the users will get stuck in the database.
In case this is needed, this is my Connection Id Generator class:
public class MyConnectionFactory : IConnectionIdGenerator
{
public string GenerateConnectionId(IRequest request)
{
if (request.Cookies["srconnectionid"] != null)
{
return request.Cookies["srconnectionid"].ToString();
}
return Guid.NewGuid().ToString();
}
}

I think your connection factory is indeed the problem. If the case that you do not find a cookie, you go ahead and generate a new guid, but by that time it's already too late.
My understanding is that the connection id is established by the client (the client side hub) during initialization and cannot be changed at the server; it can only be read. In effect when you are returning a new Guid when you don't find the cookie you are changing the client id.
In my connection factory if the cookie is not found I throw. In the controller action that opens the page that is using signalr I make sure the cookie is planted.
Here is my connection factory:
public class ConnectionFactory : IConnectionIdGenerator
{
public string GenerateConnectionId(IRequest request)
{
if (request.Cookies["UserGuid"] != null)
return request.Cookies["UserGuid"].Value;
throw new ApplicationException("No User Id cookie was found on this browser; you must have cookies enabled to enter.");
}
}

Related

Authentication for MVC and WebAPI using customer user, role tables

I need to create a authentication for my MVC Application and WebAPI.
I have the user credential details & role information in a separate table in database. Can anyone suggest which model i can use to achieve this.
Thanks
Which Web Api are you using if it is 2 than try below code, and let me know if i could help you more, because i had same scenario like you have
you have to create a custom authorization filter and call it above ActionMethod,
Create a different class in your project and change build mode in Compile
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
public static bool VaidateUserRoleWise(string username, string password, int RoleId)
{
//DO DATABASE CONNECTION DO QUERY HERE
if (Username == username && Password == password)
{
return true;
}
else
{
return false;
}
}
public override void OnAuthorization(QuizzrApi.Controllers.QuizzrController.InputParamAdminLogin LoginDetails)
{
System.Web.Http.Controllers.HttpActionContext actionContext = null;
if (LoginDetails == null)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
else
{
//Bellow is the static method called above will return true or false if user matches
if (!VaidateUserRoleWise(LoginDetails.UserName, LoginDetails.Password, 1))
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
base.OnAuthorization(actionContext);
}
}
In controller :
[Route("AuthorizeSystemAdmin")]
[HttpPost]
[BasicAuthentication]
public HttpResponseMessage Login([FromBody] InputParamAdminLogin AdminLoginInput)
{
//do your logic here
}

signalR2: OnDisconnected fires immediately on page refresh

Isnt the OnDisconnect method supposed to wait a default 30s before being fired? For me it fires instantly on page refresh(F5).
I have a User object which keeps track of a users connections in a hashset.
In my hub I have a dictionary to keep track of connected users.
OnConnected: I add that user to the dictionary, if the user is already there, I just add another connectionid to the users hashset.
OnDisconnected: I remove that connectionId from the calling users hashset, and if he doesnt have any connections left I remove the user object from the dictionary.
I need to keep track of the user object, and I lose it on every page refresh(F5) cause OnDisconnected gets fired straight away and removes the users only connection and the object. And when the page loads again, a new user object gets created, cause the old one was removed straight away.
My Implementation looks something like this
private static readonly ConcurrentDictionary<string, User> Users
= new ConcurrentDictionary<string, User>();
public override Task OnConnected() {
string userName = Context.User.Identity.Name;
string connectionId = Context.ConnectionId;
var user = Users.GetOrAdd(userName, _ => new User {
Name = userName,
ConnectionIds = new HashSet<string>()
});
lock (user.ConnectionIds) {
user.ConnectionIds.Add(connectionId);
// TODO: Broadcast the connected user
}
return base.OnConnected();
}
public override Task OnDisconnected() {
string userName = Context.User.Identity.Name;
string connectionId = Context.ConnectionId;
User user;
Users.TryGetValue(userName, out user);
if (user != null) {
lock (user.ConnectionIds) {
user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));
if (!user.ConnectionIds.Any()) {
User removedUser;
Users.TryRemove(userName, out removedUser);
// You might want to only broadcast this info if this
// is the last connection of the user and the user actual is
// now disconnected from all connections.
Clients.Others.userDisconnected(userName);
}
}
}
return base.OnDisconnected();
}
So I solved this by running a task inside the OnDisconnected method and delaying the method by x Seconds then checking if the user has reconnected, if he hasn't remove him from the list.
public override Task OnDisconnected(bool stopCalled)
{
Task mytask = Task.Run(() =>
{
UserDisconnected(Context.User.Identity.Name, Context.ConnectionId);
});
return base.OnDisconnected(stopCalled);
}
private async void UserDisconnected(string un, string cId)
{
await Task.Delay(10000);
string userName = un;
string connectionId = cId;
User user;
enqueuedDictionary.TryGetValue(userName, out user);
if (user != null)
{
lock (user.ConnectionIds)
{
user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));
if (!user.ConnectionIds.Any())
{
User removedUser;
enqueuedDictionary.TryRemove(userName, out removedUser);
ChatSession removedChatSession;
groupChatSessions.TryRemove(userName, out removedChatSession);
UpdateQ(removedUser.QPos);
}
}
}
}

Managing SignalR connections for Anonymous user

I am using SignalR version 2.1.2 with ASP.Net MVC 5 & NServiceBus and have following requirement
There is a signup page (anonymous authentication) in which SignalR is used to send notifications. Every form submit will generate a new connection id which needs to be kept in a collection so that I can send response to the client. Context.User.Identity.Name is empty hence _connections.Add(name, Context.ConnectionId); cannot be used in OnConnected() hub event as given in this post
Similar problem exists in Login page.
If there is a possibility to control the ConnectionId then I could overcome this situation but it looks like new version of SignalR has got rid of connection factory.
I am using Redis cache so one option is to write my own connection management code to keep these connection ids in it.
Second option is to use Forms Authentication in such a way that a 'Anonymous Role' is assigned to these users which restricts the usage to anonymous views/controllers but gives a 'Name' to the user so that Context.User.Identity.Name is not empty. With this I can use built in SignalR mechanism to manage connection ids for me.
This is what we did in BaseAnonymousController
public class BaseAnonymousController : Controller
{
protected override void OnAuthentication(System.Web.Mvc.Filters.AuthenticationContext filterContext)
{
if (filterContext.Controller.GetType().Name == "AccountController" && filterContext.ActionDescriptor.ActionName == "login")
{
Guid result;
if (!string.IsNullOrEmpty(SessionVariables.UserId) && Guid.TryParse(SessionVariables.UserId, out result))
{
//Already a anonymous user, so good to go.
}
else
{
//Seems to be a logged in a user. So, clear the session
Session.Clear();
}
}
//Perform a false authentication for anonymous users (signup, login, activation etc. views/actions) so that SignalR will have a user name to manage its connections
if (!string.IsNullOrEmpty(SessionVariables.UserId))
{
filterContext.HttpContext.User = new CustomPrincipal(new CustomIdentity(SessionVariables.UserId, "Anonymous"));
}
else
{
string userName = Guid.NewGuid().ToString();
filterContext.HttpContext.User = new CustomPrincipal(new CustomIdentity(userName, "Anonymous"));
FormsAuthentication.SetAuthCookie(userName, false);
SessionVariables.UserId = userName;
}
base.OnAuthentication(filterContext);
}
}
and used this class as base class for all of anonymous controllers.
public class AccountController : BaseAnonymousController
{
[AllowAnonymous]
public ActionResult Signup()
{
//Your code
}
[AllowAnonymous]
public ActionResult Login()
{
//Your code
}
[AllowAnonymous]
public ActionResult ForgotPassword()
{
//Your code
}
[AllowAnonymous]
public ActionResult ForgotUsername()
{
//Your code
}
}
In the SignalR hub (nothing extraordinary than what is in SignalR documentation)
public override Task OnConnected()
{
SignalRConnectionStore.Add(Context.User.Identity.Name, Context.ConnectionId);
return base.OnConnected();
}
public override Task OnReconnected()
{
string name = Context.User.Identity.Name;
//Add the connection id if it is not in it
if (!SignalRConnectionStore.GetConnections(name).Contains(Context.ConnectionId))
{
SignalRConnectionStore.Add(name, Context.ConnectionId);
}
return base.OnReconnected();
}
public override Task OnDisconnected(bool stopCalled)
{
SignalRConnectionStore.Remove(Context.User.Identity.Name, Context.ConnectionId);
return base.OnDisconnected(stopCalled);
}
This works for both anonymous and authenticated users.
SignalRConnectionStore class and Interface
public interface ISignalRConnectionStore
{
int Count { get; }
void Add(string userName, string connectionId);
IEnumerable<string> GetConnections(string userName);
void Remove(string userName, string connectionId);
}
internal class SignalRConnectionStore : ISignalRConnectionStore
{
private readonly Dictionary<string, HashSet<string>> _connections = new Dictionary<string, HashSet<string>>();
public int Count
{
get
{
return _connections.Count;
}
}
public void Add(string userName, string connectionId)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(connectionId))
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(userName, out connections))
{
connections = new HashSet<string>();
_connections.Add(userName, connections);
}
lock (connections)
{
connections.Add(connectionId);
}
}
}
}
public IEnumerable<string> GetConnections(string userName)
{
if (!string.IsNullOrEmpty(userName))
{
HashSet<string> connections;
if (_connections.TryGetValue(userName, out connections))
{
return connections;
}
}
return Enumerable.Empty<string>();
}
public void Remove(string userName, string connectionId)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(connectionId))
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(userName, out connections))
{
return;
}
lock (connections)
{
connections.Remove(connectionId);
if (connections.Count == 0)
{
_connections.Remove(userName);
}
}
}
}
}
}
Declare a static variable of SignalRConnectionStore in Hub class as below.
public class ProvisioningHub : Hub
{
private static ISignalRConnectionStore SignalRConnectionStore;
public ProvisioningHub(ISignalRConnectionStore signalRConnectionStore)
: base()
{
SignalRConnectionStore = signalRConnectionStore; //Injected using Windsor Castle
}
}
Use Forms Authentication, store a Federated Cookie and store the hub region in the cookie as well..
In SignalR jQuery code, use a jQuery plugin to read HTTP cookie and get the region name and subscribe to notifications.
Alternatively, in your .cshtml, render jQuery with region populated from your View Model.
Note: Use FormsAuthentication.SetAuthCookie as this will create HTTP Only cookie and will be sent in Ajax and non-Ajax calls.

SignalR Javascript Client: Cannot Start Connection

My connection does not start.
This code worked in 1.x but in version 2 is not working.
SignalR seems to be trying to connect but without success.
The hub method is never called.
Attached sending an image with SignalR debug.
Javascript:
<script type="text/javascript">
$.connection.hub.logging = true;
var options = { transport: ['webSockets', 'longPolling'] };
$(function() {
var userHub = $.connection.userHub;
//Iniciar connecção
window.hubReady = $.connection.hub.start(options);
window.hubReady.done(function () {
userHub.server.ini();
});
userHub.client.iniDone = function (connectionId) {
console.log(connectionId);
};
$.connection.hub.connectionSlow(function() {
console.log('slow connection...');
});
window.hubReady.fail(function(error) {
console.log(error);
});
$.connection.hub.disconnected(function() {
setTimeout(function() {
$.connection.hub.start();
}, 2000);
});
});
</script>
Hub:
[HubName("userHub")]
public class UserHub : Hub
{
public void Ini()
{
Clients.Client(Context.ConnectionId).iniDone(string.Format("Conectado com o id: {0}", Context.ConnectionId));
}
public override Task OnConnected()
{
var connectionId = Context.ConnectionId;
var email = string.IsNullOrWhiteSpace(Context.User.Identity.Name) ? Context.Headers["email"] : Context.User.Identity.Name;
if (email != null && connectionId != null)
UserData.GetInstance(email).ConnectionsIds.Add(connectionId);
return base.OnConnected();
}
public override Task OnDisconnected()
{
var connectionId = Context.ConnectionId;
var email = string.IsNullOrWhiteSpace(Context.User.Identity.Name) ? Context.Headers["email"] : Context.User.Identity.Name;
if (email != null && connectionId != null)
UserData.GetInstance(email).ConnectionsIds.Remove(connectionId);
return base.OnDisconnected();
}
}
Debug:
SignalR Debug Image
EDIT:
I found the problem! The GetInstance method of my Singleton has problems.
public static UserData GetInstance(string username)
{
if (_sharedUsers == null)
lock (_lockCreate)
_sharedUsers = new Dictionary<string, UserData>();
if (!_sharedUsers.ContainsKey(username))
lock (_lockAdd)
_sharedUsers.Add(username, new UserData(username));
return _sharedUsers[username];
}
the method stops always here: lock (_lockAdd)
I want to save all user connectionsIds Any ideas?
Thanks
Try moving the client method subscription to be before you connect. If it's not registered by the time the connection is started, then it will not be callable from the server.
So change it to the following:
$(function() {
var userHub = $.connection.userHub;
//Register Client handlers first
userHub.client.iniDone = function (connectionId) {
console.log(connectionId);
};
//Now you can connect.
window.hubReady = $.connection.hub.start(options);
window.hubReady.done(function () {
userHub.server.ini();
});
$.connection.hub.connectionSlow(function() {
console.log('slow connection...');
});
window.hubReady.fail(function(error) {
console.log(error);
});
$.connection.hub.disconnected(function() {
setTimeout(function() {
$.connection.hub.start();
}, 2000);
});
});
Edit
Based on your comment around a server error in the OnConnected method, it seems like you may have a two problems then. Isolate the connection tracking part out (just comment it out) to get the full round-trip going between client and server. Then add back the connection tracking which is possibly a DB connection error - check the server logs.
Edit
In terms of storing the user connections, you've a few options.
Use ConcurrentDictionary:
One of the simplest is storing in a static ConcurrentDictionary, similar to what you have. Try to avoid the use of so many locks - using a ConcurrentDictionary means you'll actually end up with none.
e.g.
public class UserData
{
public UserData(string username)
{
UserName = username;
ConnectionIds = new HashSet<string>();
}
public string UserName { get; private set; }
public HashSet<string> ConnectionIds { get; private set; }
}
public static class ConnectionStore
{
private static readonly ConcurrentDictionary<string, UserData> _userData = new ConcurrentDictionary<string, UserData>();
public static void Join(string username, string connectionId)
{
_userData.AddOrUpdate(username,
u => new UserData(u), /* Lambda to call when it's an Add */
(u, ud) => { /* Lambda to call when it's an Update */
ud.ConnectionIds.Add(connectionId);
return ud;
});
}
}
See MSDN for more info: http://msdn.microsoft.com/en-us/library/ee378675(v=vs.110).aspx
Use a database:
The other option is to store in a database (using Entity Framework) which has the added benefit of tracking user data across server recycles.
Have a look at http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections which shows all these options a couple of others.
Had the same problem for so long, so gave up the whole signalR at some point, but had to pick it up again for our project:
I have written an answer which might lead you and others on the right track (step by step)...In the answer I am using PersistentConnection rather than Hub, but the principle should be the same:
https://stackoverflow.com/a/25304790/3940626

How to obtain connection ID of signalR client on the server side?

I need to get the connection ID of a client. I know you can get it from the client side using $.connection.hub.id. What I need is to get in while in a web service I have which updates records in a database, in turn displaying the update on a web page. I am new to signalR and stackoverflow, so any advice would be appreciated. On my client web page I have this:
<script type="text/javascript">
$(function () {
// Declare a proxy to reference the hub.
var notify = $.connection.notificationHub;
// Create a function that the hub can call to broadcast messages.
notify.client.broadcastMessage = function (message) {
var encodedMsg = $('<div />').text(message).html();// Html encode display message.
$('#notificationMessageDisplay').append(encodedMsg);// Add the message to the page.
};//end broadcastMessage
// Start the connection.
$.connection.hub.start().done(function () {
$('#btnUpdate').click(function () {
//call showNotification method on hub
notify.server.showNotification($.connection.hub.id, "TEST status");
});
});
});//End Main function
</script>
everything works up until I want to update the page using signalR. The show notification function in my hub is this:
//hub function
public void showNotification(string connectionId, string newStatus){
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<notificationHub>();
string connection = "Your connection ID is : " + connectionId;//display for testing
string statusUpdate = "The current status of your request is: " + newStatus;//to be displayed
//for testing, you can display the connectionId in the broadcast message
context.Clients.Client(connectionId).broadcastMessage(connection + " " + statusUpdate);
}//end show notification
how can I send the connectionid to my web service?
Hopefully I'm not trying to do something impossible.
When a client invokes a function on the server side you can retrieve their connection ID via Context.ConnectionId. Now, if you'd like to access that connection Id via a mechanism outside of a hub, you could:
Just have the Hub invoke your external method passing in the connection id.
Manage a list of connected clients aka like public static ConcurrentDictionary<string, MyUserType>... by adding to the dictionary in OnConnected and removing from it in OnDisconnected. Once you have your list of users you can then query it via your external mechanism.
Ex 1:
public class MyHub : Hub
{
public void AHubMethod(string message)
{
MyExternalSingleton.InvokeAMethod(Context.ConnectionId); // Send the current clients connection id to your external service
}
}
Ex 2:
public class MyHub : Hub
{
public static ConcurrentDictionary<string, MyUserType> MyUsers = new ConcurrentDictionary<string, MyUserType>();
public override Task OnConnected()
{
MyUsers.TryAdd(Context.ConnectionId, new MyUserType() { ConnectionId = Context.ConnectionId });
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
MyUserType garbage;
MyUsers.TryRemove(Context.ConnectionId, out garbage);
return base.OnDisconnected(stopCalled);
}
public void PushData(){
//Values is copy-on-read but Clients.Clients expects IList, hence ToList()
Clients.Clients(MyUsers.Keys.ToList()).ClientBoundEvent(data);
}
}
public class MyUserType
{
public string ConnectionId { get; set; }
// Can have whatever you want here
}
// Your external procedure then has access to all users via MyHub.MyUsers
Hope this helps!
Taylor's answer works, however, it doesn't take into consideration a situation where a user has multiple web browser tabs opened and therefore has multiple different connection IDs.
To fix that, I created a Concurrent Dictionary where the dictionary key is a user name and the value for each key is a List of current connections for that given user.
public static ConcurrentDictionary<string, List<string>> ConnectedUsers = new ConcurrentDictionary<string, List<string>>();
On Connected - Adding a connection to the global cache dictionary:
public override Task OnConnected()
{
Trace.TraceInformation("MapHub started. ID: {0}", Context.ConnectionId);
var userName = "testUserName1"; // or get it from Context.User.Identity.Name;
// Try to get a List of existing user connections from the cache
List<string> existingUserConnectionIds;
ConnectedUsers.TryGetValue(userName, out existingUserConnectionIds);
// happens on the very first connection from the user
if(existingUserConnectionIds == null)
{
existingUserConnectionIds = new List<string>();
}
// First add to a List of existing user connections (i.e. multiple web browser tabs)
existingUserConnectionIds.Add(Context.ConnectionId);
// Add to the global dictionary of connected users
ConnectedUsers.TryAdd(userName, existingUserConnectionIds);
return base.OnConnected();
}
On disconnecting (closing the tab) - Removing a connection from the global cache dictionary:
public override Task OnDisconnected(bool stopCalled)
{
var userName = Context.User.Identity.Name;
List<string> existingUserConnectionIds;
ConnectedUsers.TryGetValue(userName, out existingUserConnectionIds);
// remove the connection id from the List
existingUserConnectionIds.Remove(Context.ConnectionId);
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
if(existingUserConnectionIds.Count == 0)
{
// if there are no connections for the user,
// just delete the userName key from the ConnectedUsers concurent dictionary
List<string> garbage; // to be collected by the Garbage Collector
ConnectedUsers.TryRemove(userName, out garbage);
}
return base.OnDisconnected(stopCalled);
}
I beg to differ on the reconnect. The client remains in the list but the connectid will change. I do an update to the static list on reconnects to resolve this.
As Matthew C is not completely thread safe in situation of one user request multiple connection at same time, I used this code:
public static Dictionary<string, List<string>> ConnectedUsers = new ();
public override Task OnConnected()
{
var connectionId = Context.ConnectionId;
var userId = Context.User.Identity.Name; // any desired user id
lock(ConnectedUsers)
{
if (!ConnectedUsers.ContainsKey(userId))
ConnectedUsers[userId] = new();
ConnectedUsers[userId].Add(connectionId);
}
}
public override Task OnDisconnected(bool stopCalled)
{
var connectionId = Context.ConnectionId;
var userId = Context.User.Identity.Name; // any desired user id
lock (ConnectedUsers)
{
if (ConnectedUsers.ContainsKey(userId))
{
ConnectedUsers[userId].Remove(connectionId);
if (ConnectedUsers[userId].Count == 0)
ConnectedUsers.Remove(userId);
}
}
}

Categories