I am using Redis backplane with SignalR. I have a problem but I couldn't find anything about this.
SignalR clients are Windows Forms application.
I'm using OnConnected and OnDisconnected events for handle user connections.
I have a POCO class for connections with ConnectionId property and some aother informations(UserName etc.).
In OnConnected event i insert new record to Redis with ConnectionId and OnDisconnected event remove the entry from Redis.
I dont'do anything in OnReconnected event.
But now there are too many records in Redis with same user names and different connectionid s.
Can i check the connection is alive with ConnectionId? If it's possible i want to delete died connections in Redis.
Do you have any idea about this same record problem?
Edit:
First, i have a mistake about OnConnected event. I don't use OnConnected event. WinForms client applications fire Join event on the Hub, after SignalR has been connected.
This is UserData POCO Class:
public class UserData
{
public string Id { get; set; }
public DateTime Connected { get; set; }
public string UserName { get; set; }
public string MachineName { get; set; }
public string ConnectionId { get; set; }
}
Join Method :
public void Join(UserData userData)
{
_trackUser.Add(userData);
}
I'm injecting _trackuser object to Hub class in Startup via
GlobalHost.DependencyResolver.Register
OnDisconnected Event :
public override Task OnDisconnected(bool b)
{
_trackUser.Remove(Context.ConnectionId);
return base.OnDisconnected(b);
}
TrackUser Add Method :
public void Add(UserData userData)
{
IRedisClient redisClient = Global.RedisClientManager.GetClient();
using (redisClient)
{
IRedisTypedClient<UserData> redisTypedClient = redisClient.As<UserData>();
UserData store = redisTypedClient.Store(userData);
}
}
TrackUser Remove Method :
public void Remove(string connectionId)
{
IRedisClient redisClient = Global.RedisClientManager.GetClient();
using (redisClient)
{
IRedisTypedClient<UserData> redisTypedClient = redisClient.As<UserData>();
bool removeEntry = redisTypedClient.RemoveEntry("urn:userdata:"+connectionId);
}
}
Keep the user connection status in a separate Database table like User table having foreign key in Connections table. Set the connection-id status in connections table from your Hub for OnConnected and OnDisconnected events.This helps to track the user connection status.
Redis backplane is purely for scaling out your application to support users connecting to different nodes over load balance environment.
Related
SignalR Chat - based on:
jQuery 3.4.1
SignalR 2.2.2
Asp.Net Framework Web Application
I'm trying to maintain a list of who is connected in the chat. It's a list of 'user' objects, the user object being the container to store 'name' and their assigned 'color' (red, blue, or green, etc color of text)
So the server model is an object with a List member:
public class GroupMemberModel
{
[JsonProperty("name")]
public string UserName { get; set; }
[JsonProperty("color")]
public string Color { get; set; }
[JsonProperty("avatar")]
public string AvatarImagePath { get; set; }
}
public class MessageModel
{
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("user")]
public GroupMemberModel User { get; set; }
[JsonProperty("group")]
public List<GroupMemberModel> ChatGroup { get; set; }
}
Then when the user sends a message, the server checks if this new user is in the list, and adds it if not:
public void Send(MessageModel clientModel)
{
if (!clientModel.ChatGroup.Contains(clientModel.User))
{
clientModel.ChatGroup.Add(clientModel.User);
clientModel.ChatGroup.Sort();
}
// Call the broadcastMessage method to update clients.
Clients.All.broadcastMessage(clientModel);
}
My issue is that anytime a new user joins, their initial 'knowledge' of the ChatGroup list of GroupMemberModel users is empty. So when a new user joins, the of 'participants' is always replaced with the single user who just joined... And this also happens between users already in the chat - the last person who left a message is the only person in the 'Participants' list.. So the list isn't being maintained at all.
So do I need to serialize / de-serialize the List ChatGroup member of MessageModel each time the server serverHub 'send' function is hit?
I thought SignalR would manage the objects on the server somehow without hitting a storage mechanism? I guess they are unique to each server call and that's why they're empty for every user? Hmmm - maybe the list has to be static?
SignalR manages users internally in its own group structures. You can't access it directly but just through a few accessors they provide that won't do what you want.
I did something very similar.
For the List, I used a static dictionary. It has to be static or a Singleton since a new hub is created for every connection. A Hub is like a webpage where a new instance is created when the client connects and destroyed when they disconnect. Any variable local to the Hub is created and destroyed with it.
static readonly ConcurrentDictionary<string, GroupMemberModel> Members; // string is the connection ID that is unique for every connection
In the Hub, I added overloads for OnConnect, OnDisconnect, and OnReconnect that check to see if the user is present and add or remove the user from the Members list. Each method will have a Context object with the data for the connected client that you can use to add or remove members.
For example:
public override Task OnConnected()
{
AddNewMember(); // add member to list, here you can check any security, header, etc.
// Note: Context contains the connected member
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
var member = GetMember();
if (member != null)
{
Members.TryRemove(member.ConnectionId, out _);
}
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
var member = GetMember(); // find the connection in the list
if (item == member)
{
var user = AddNewMember();
}
return base.OnReconnected();
}
This way, your server always maintains a list of connected members.
I get following error from an Azure Function App when using cosmos DB. I have got the same with HttpClient but seemed to solve that by doing HttpClient static. Can you solve the same problem just by making the CosmosDB client static? Something like:
public class DocRepoCoach
{
public string ConnStr { get; set; }
public Container XX1Container { get; set; }
public Container XX2Container { get; set; }
**public static CosmosClient Client { get; set; }**
public DocRepoCoach(string connectionString)
{
ConnStr = connectionString;
var options = new CosmosClientOptions() { AllowBulkExecution = true, MaxRetryAttemptsOnRateLimitedRequests = 1000 };
Client = new CosmosClient(ConnStr, options);
XX1Container = Client.GetContainer("XXXAPI", "XX");
XX2Container = Client.GetContainer("XXXAPI", "XX");
}
}
Yes, please make it static.
The recommended practice with Azure functions is to use a Singleton client for the lifetime of your application. The CosmosClient can manage connections when you use a static client.
Below are the recommendations
Do not create a new client with every function invocation.
Do create a single, static client that every function invocation can use.
Consider creating a single, static client in a shared helper class if different functions use the same service.
These are also documented here on Azure docs
All my SignalR clients connect using a JWT bearer token. I utilize the [Authorize] attribute in my SignalR Hub.
This token contains a userId which can be used to check if a user has read access on the resource through the resource's users property which contains a List<PuppyUserPermission> that look like this:
public class PuppyUserPermission
{
public string userId { get; set; }
public bool read { get; set; }
public bool write { get; set; }
}
The question is: how do I connect the dots here? Ideally, instead of something like
[Authorize]
public class PuppyHub : Hub
{
public async Task SendPuppy(Puppy pup)
{
await Clients.All.SendAsync(pup);
}
}
I would so something like the following (this is more pseudo code than anything else, as I don't use valid methods):
[Authorize]
public class PuppyHub : Hub
{
public async Task SendPuppy(Puppy pup)
{
var clients = Puppy.users.Where(u => u.read == true);
await clients.SendAsync(pup);
}
}
Basically, I'd like to ensure that the clients recieving the Puppy object via SignalR would be authorized users on the resource. Problem is, Clients is just a list of string client IDs, and I'm not sure how to go about tying them to actual users on my Puppy resource.
How do I go about achieving this?
From the beginning, I had the feeling that the answer lay in IUserIdProvider, but I didn't see how that would work for multiple users.
I finally found the answer, but it'll definitely need some cleanup.
First, create your own implementation of IUserIdProvider as follows:
public class MyUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
var username = connection.User.Claims.Where(x => x.Type == "THE_CLAIM_YOU_WANT_TO_USE_TO_IDENTIFY_USERS").First().Value;
return username;
}
}
Next, register it using DI:
services.AddSingleton<IUserIdProvider, MyUserIdProvider >();
Now, when you want to send events from the server, use DI in your constructor to pull down an instance of your SignalR Hub as per usual:
private IHubContext<PuppyHub> puppyHub { get; }
public UsersController(IHubContext<PuppyHub> _puppyHub)
{
puppyHub = _puppyHub;
}
Then, where when you want to tell your clients about the new Puppy:
// ... typical controller code
// assume we have a var, puppy, with a list of authorized users
// use System.Linq to get a list of userIds where the user is authorized to read the puppy
var authorizedUsers = (IReadOnlyList<string>)puppy.users.Where(x => x.permissions.read == true).Select(i => i._id).ToList();
// send the new puppy to the authorized users
await puppyHub.Clients.Users(authorizedUsers).SendAsync("SendPuppy", puppy);
And viola! You have now done resource-based authorization with SignalR.
I am trying to send notifications to specific clients from server about transaction changes. I'm doing well so far in local and development environment, but in LIVE we have 3 app pools (3 physical servers) with loadbalancer (all three have same machine key).
So it looks like when notification is triggered server side on same pool the client is connected - it works, but doesn't work if pools are different.
Can anyone suggest how to deal with this - or maybe the problem is in bad code (incorrect hub context handling or something). I'm new to SignalR - so used examples for message broadcasting from SignalR documentation.
Here is mu hub:
public class ExampleClassHub : Hub
{
private readonly ExampleClass _ExampleClassInstance;
public ExampleClassHub() : this(ExampleClass.Instance) { }
public ExampleClassHub(ExampleClass ExampleClassInstance)
{
_ExampleClassInstance = ExampleClassInstance;
}
}
Here is the class which serves static instance:
public class ExampleClass
{
private readonly static Lazy<ExampleClass> _instance = new Lazy<ExampleClass>(() =>
new ExampleClass(GlobalHost.ConnectionManager.GetHubContext<ExampleClassHub>().Clients));
private IHubConnectionContext<dynamic> Clients { get; set; }
private ExampleClass(IHubConnectionContext<dynamic> clients)
{
Clients = clients;
}
public static ExampleClass Instance
{
get
{
return _instance.Value;
}
}
public void NotifyTransactionChange(int userId, string tid, bool isTransactionSuccessfull)
{
string json = JsonConvert.SerializeObject(new Notification { UserId = userId, Tid = tid, IsTransactionSuccessful = isTransactionSuccessfull });
Clients.User(userId.ToString()).notifyTransactionStateChange(json);
}
class Notification
{
public int UserId { get; set; }
public string Tid { get; set; }
public bool IsTransactionSuccessful { get; set; }
}
}
So from server-side notification is triggered the following way:
ExampleClass.Instance.NotifyTransactionChange(...)
Solved using SignalR scaleout:
http://www.asp.net/signalr/overview/performance/scaleout-in-signalr
Consider a web application like facebook, that can send realtime notifications to users.
What is the best way, using asp.net SignalR, to keep track of which connection ids belong to which user, even if the user disconnects, or reconnects later ?
Check out the following blog post:
Mapping ASP.NET SignalR Connections to Real Application Users
Briefly, you would be adding the connection ids to user on the OnConnected method and remove that connection on the OnDisconnected method. Keep in mind that an application user can have multiple connections. So, you need to have a one to many relationship between your user and the connection ids. The above linked post explains it in details with a sample.
I did this for an internal app. The way I did it is that when a user connects, I have the server ask the user to register themselves. This way I know that not only a user is connected and their signalR connnectionID, but they can also tell me any other information (like username or whatever).
When they reconnect I ask them to do it again.
SignalR will maintain the same connectionID per client even if they reconnect which is nice. A reconnection is not the same as an initial connection. New connections indicate a new client, but a reconnection is on the same client.
In my app I maintained a seperate threadsafe dictionary that I kept track of which user and which connectionID was doing what. This way I can say "oh, send message to user ABC" and look up their connectionID. Then act on the Hub's clients object in signalR for that connectionID. If you do it this way you can even have the same "user" in mutliple connections. Imagine user "abc" is open in two browser tabs. If you went strictly by connectionID they'd be technically two different users. But, by maintaining some sort of local collection grouping users and connections you can now have multiple connections for the same user.
I should mention that if you do it this way, you should make sure your site handles what happens when it restarts and loses all the connection information. For me, when someone reconnects I ask them to again re-identify themselves. This way I can re-build my local dictionary when the server comes online without worry. It does have more overhead because now you are asking all your clients to send information to you, but depending on your user case this could be staggered or bunched or otherwise distributed to help you handle load.
In general, however you get local information (whether by asking the user to supply it), or by http context session info, you need to track it yourself.
Well I used a different approach, I extended the ApplicationUser class like that:
// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
public class ApplicationUser : IdentityUser
{
//public int ApplicationUserId { get; set; }
//public string Name { get; set; }
//public string Address { get; set; }
//public string City { get; set; }
//public string State { get; set; }
//public string Zip { get; set; }
[Required]
public string Email { get; set; }
[Required]
public override string UserName { get; set; }
[NotMapped]
public string ConnectionId { get; set; }
[NotMapped]
public string ChattingUserConnectionId { get; set; }
//public string HomeTown { get; set; }
//public DateTime? BirthDate { get; set; }
}
And in my hub i'm doing something like that:
public class ChatHub : Hub
{
#region Data Members
private static ApplicationDbContext applicationDbContext = new ApplicationDbContext();
private static UserManager<ApplicationUser> userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(applicationDbContext));
private static List<ApplicationUser> connectedUsers = new List<ApplicationUser>();
And when a user connects to the chat, I get his ApplicationUser object by his user name and add it to the connectedUsers list. When he disconnects, I remove him.
I ran into some random exceptions with EF states and such which made me create the ApplicationDbContext and UserManager each time it is accessed instead of setting it in a static object:
private ApplicationUser GetCurrentUser()
{
ApplicationDbContext applicationDbContext = new ApplicationDbContext();
UserManager<ApplicationUser> userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(applicationDbContext));
var userName = Context.User.Identity.GetUserName();
var user = userManager.FindByName<ApplicationUser>(userName);
return user;
}
Edit:
The hub code has some problems loading child objects of the user. This code which is also used in the asp.net template will work better, ApplicationDBContext is not needed:
private ApplicationUserManager _userManager
{
get
{
return HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
}
var user = _userManager.FindByName<ApplicationUser, string>(userName);
There's a very good article on ASP.NET's SignalR tutorial section.
I've included the introductory paragraph below.
Mapping SignalR Users to Connections
Each client connecting to a hub passes a unique connection id. You can
retrieve this value in the Context.ConnectionId property of the hub
context. If your application needs to map a user to the connection id
and persist that mapping, you can use one of the following:
The User ID Provider (SignalR 2)
In-memory storage, such as a dictionary
SignalR group for each user
Permanent, external storage, such as a database table or Azure table
storage Each of these implementations is shown in this topic. You use
the OnConnected, OnDisconnected, and OnReconnected methods of the Hub
class to track the user connection status.
UserID Provider
If you're using ASP.NET's standard membership provider (IPrincipal.Identity.Name) you can just do the following to access a client based on a user account. If you have your own user system you can create your own provider.
public class MyHub : Hub
{
public void Send(string userId, string message)
{
Clients.User(userId).send(message);
}
}