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);
}
}
}
}
Related
I am trying to call Firebase functions inside a "DAL" class as I want to create some abstraction. I cannot figure out how to return a variable from inside the callback, or how to pass in a callback to execute from the controller class I created. How would I make UserExistsInFirebase async and call that from a controller class like
public void GetUser() {
bool exists = await firebase.UserExistsInFirebase("User1");
}
public Task<bool> UserExistsInFirebase(string Username)
{
bool isExists = false;
reference.Child("Users").Child(Username).Child("Username").Child(Username).GetValueAsync().ContinueWithOnMainThread(task =>
{
if (task.IsCompleted)
{
DataSnapshot snapshot = task.Result;
//checks to see if we found another user with the same username
if (snapshot.GetValue(true) != null)
{
isExists = true;
}
}
});
return isExists;
}
In your current code the issue is that ContinueWithInMainThread is callback called some time later once the task is finished. In the meantime it doesn't block your UserExistsInFirebase at all which rather directly continues and returns false.
Note btw that a method would need to be async in order to be able to use await within it ;)
So instead of ContinueWithInMainThread you would rather make your task async and use await once more.
public async Task<bool> UserExistsInFirebase(string Username)
{
var snapShot = await reference.Child("Users").Child(Username).Child("Username").Child(Username).GetValueAsync();
return snapShot.GetValue(true) != null;
}
Now this would then not be returned in the main thread though ;)
You can however again ensure that by using e.g.
public void GetUser()
{
firebase.UserExistsInFirebase("User1").ContinueWithOnMainThread(exists =>
{
// Do something with the bool exists after the request has finished
});
// again anything here would be immediately executed before the request is finihed
}
so basically you have just abstracted the task but keep the handling of the callback on the top level where you are starting your request.
If you still want the error handling like before you could probably also do something like
public async Task<bool> UserExistsInFirebase(string Username)
{
bool isExists = false;
await reference.Child("Users").Child(Username).Child("Username").Child(Username).GetValueAsync().ContinueWith(task =>
{
if (task.IsCompleted)
{
DataSnapshot snapshot = task.Result;
//checks to see if we found another user with the same username
if (snapshot.GetValue(true) != null)
{
isExists = true;
}
}
});
return isExists;
}
I have created a chat using SignalR2. The client and server itself works fine. Now, I'm trying to implement a 'users online' function. The server code seems about right, but I'm struggling to make the client receive the data that the server pushes back to the client.
This is the server code below:
public static List<string> Users = new List<string>();
public void Send(string name, string message)
{
// Call the broadcastMessage method to update clients.
Clients.All.broadcastMessage(name, message);
Clients.All.addMessage(name, message);
}
public void SendUserList(List<string> users)
{
var context = GlobalHost.ConnectionManager.GetHubContext<chatHub>();
context.Clients.All.updateUserList(users);
}
public override Task OnConnected()
{
string clientId = GetClientId();
//if (Users.IndexOf(clientId) == -1)
//{
Users.Add(clientId);
//}
SendCount(Users.Count);
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
System.Diagnostics.Debug.WriteLine("Disconnected");
SendCount(Users.Count);
return base.OnDisconnected(stopCalled);
}
private string GetClientId()
{
string clientId = "";
if (Context.QueryString["clientId"] != null)
{
// clientId passed from application
clientId = this.Context.QueryString["clientId"];
}
if (string.IsNullOrEmpty(clientId.Trim()))
{
clientId = Context.ConnectionId;
}
return clientId;
}
public void SendCount(int count)
{
// Call the addNewMessageToPage method to update clients.
var context = GlobalHost.ConnectionManager.GetHubContext<chatHub>();
context.Clients.All.updateUsersOnlineCount(count);
}
Below is the client code for connecting / receiving messages:
public static async void ConnectAsync(RadChat ChatInternal)
{
ChatInternal.Author = new Author(null, Varribles.Agent);
var querystringData = new Dictionary<string, string>();
querystringData.Add("clientId", Varribles.Agent);
Connection = new HubConnection(ServerURI, querystringData);
HubProxy = Connection.CreateHubProxy("chatHub");
//Handle incoming event from server: use Invoke to write to console from SignalR's thread
HubProxy.On<string, string>("AddMessage", (name, message) =>
ChatInternal.Invoke((Action)(() =>
Backend.GET.Messages(ChatInternal)
)));
try
{
await Connection.Start();
Backend.GET.Messages(ChatInternal);
}
catch (System.Net.Http.HttpRequestException)
{
//No connection: Don't enable Send button or show chat UI
return;
}
}
Now, my question is, how can I retrieve the 'Users' list from the server?
Thanks in advance
I've implemented a custom User Store for ASP.NET Identity by following the example set here. That all works fine, except for this:
I need access to data about the currently logged in user in my user store. Normally, you'd access that by accessing
HttpContext.Current.User
Now, once auser has logged in, if he user then goes to the Manage controller (e.g. to try and change his/her password), when ASP.NET identity looks up the user again by calling
CustomUserManager.FindByIdAsync(string userId)
HttpContext.Current is empty altogether (that's prior to rendering the page). So, how do I get information about the HttpContext in this scenario? The user is properly logged in, so how do I figure out which user has been logged in?
#edit.. the problem is in CustomUserStore.. here's a bit of it
public class CustomUserStore<TUser> : IUserStore<TUser>, IUserLoginStore<TUser>, IUserClaimStore<TUser>, IUserPasswordStore<TUser>, IUserSecurityStampStore<TUser>, IUserEmailStore<TUser>, IUserPhoneNumberStore<TUser>,
IUserLockoutStore<TUser, string>, IUserTwoFactorStore<TUser, string>//, IQueryableUserStore<TUser>
where TUser: CustomUser<string>, IUser<string>
{
string storageFile = #"c:\temp\aspnetusers.json";
List<TUser> users;
public CustomUserStore()
{
if (File.Exists(storageFile))
{
string contents = File.ReadAllText(storageFile);
users = JsonConvert.DeserializeObject<List<TUser>>(contents);
if (users == null)
users = new List<TUser>();
}
else
users = new List<TUser>();
}
#region IUserStore implementation
public Task<TUser> FindByIdAsync(string userId)
{
string sessionId = HttpContext.Current?.Session?.SessionID;
return Task.FromResult<TUser>(users.FirstOrDefault(u => u.Id == userId));
}
public Task<TUser> FindByNameAsync(string userName)
{
string sessionId = HttpContext.Current?.Session?.SessionID;
return Task.FromResult<TUser>(users.FirstOrDefault(u => string.Compare(u.UserName, userName, true) == 0));
}
#endregion
}
and it's in the FindByAsync method where HttpContext.Current can be empty.
It happens in the Index method of the AccountController when the model is created
var model = new IndexViewModel
{
HasPassword = HasPassword(),
PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
Logins = await UserManager.GetLoginsAsync(userId),
BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(userId)
};
And it's the FindById request in the HasPassword method that causes the problem
private bool HasPassword()
{
var user = UserManager.FindById(User.Identity.GetUserId());
if (user != null)
{
return user.PasswordHash != null;
}
return false;
}
The other 4 requests to the user manager all have a filled out HttpContext.Current. So it appears that it's calls to UserManager that cause the issue.
Having identified the exact source of the problem, it's easy enough to fix.
Add this async emthod to check if the user has a password:
private async Task<bool> HasPasswordAsync()
{
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user != null)
{
return user.PasswordHash != null;
}
return false;
}
And in the Index method, use the new async methode
var model = new IndexViewModel
{
HasPassword = await HasPasswordAsync(),
PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
Logins = await UserManager.GetLoginsAsync(userId),
BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(userId)
};
But, why does the synchronous method call break things? You'd imagine the sync call would run into the standard context where HttpContext.Current should be available.
I have a more custom User Store in my real project where I run into this problem a lot more frequently.. guess I need to check if contains a lot more synchronous access to UserManager methods.
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);
}
}
}
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.");
}
}