SignalR chat members don't get existing array of users in chat - signalr

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.

Related

Using Backlink feature of realm-dotnet in Xamarin.Forms App

My current employer is developing a mobile app using Xamarin.Forms and Asp.net mvc on the backend. I suggested to use realm in the mobile app. My manager want to see a POC(Proof of concept) app using realm with backlink feature before allowing it to be used in the app. I am working on the POC on GitHub . The documentation is very limiting and the GitHub repo of realm-dotnet don’t have good sample.
I completed the project. But unable to implement backlink. The sample app I have developed allow user to create assignees(employees) in the first page. The user can delete or edit the employees using context menu. When the user clicks on the employee name the app navigates to the ToDoListPage of that particular employee. Here the user can create ToDoItems. On this ToDoList page I want to show the ToDoItems that where assigned to that employee only.
The models were as follows:
public class Assignee : RealmObject
{
public Assignee()
{
ToDoItems = Enumerable.Empty<ToDoItem>().AsQueryable();
}
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Name { get; set; }
public string Role { get; set; }
[Backlink(nameof(ToDoItem.Employee))]
public IQueryable<ToDoItem> ToDoItems { get; }
}
public class ToDoItem : RealmObject
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Name { get; set; }
public string Description { get; set; }
public bool Done { get; set; }
public Assignee Employee { get; set; }
}
I am adding employee to each ToDo Item:
Item.Employee = Employee;
_realm.Add(Item);
Now I want to access the ToDoItems for the Employee:
Items = _realm.All<Assignee>().Where(x => x.Id == EmployeeId).FirstOrDefault().ToDoItems;
But this does not work. I will be grateful if someone can help me out by preferably writing code in my sample app or write the correct code in the reply.
Thank you
Firstly, Realm .NET doesn't currently support traversing properties (x.Employee.Id). Due to this, when I start the app and try to go to the ToDoListPage, the app crashes with the exception:
The left-hand side of the Equal operator must be a direct access to a persisted property in Realm
Realm supports object comparison, so we can fix this like so:
var employee = _realm.Find<Assignee>(EmployeeId);
Items = _realm.All<ToDoItem>().Where(x => x.Employee == employee);
Secondly, everything seemed fine in your code, so I dug a bit deeper and saw why it isn't working. The issue is that when we try to get all items with the code above, the EmployeeId parameter is null. Since the EmployeeId is being populated after the load logic has been triggered, we don't need to load the data in the ctor. So you can remove this code.
Finally, since you won't be loading the data in the ctor, and instead in the SetValues method, the UI needs to know, when the data has been updated, what exactly to redraw. Thus, you need to mark the collection to be Reactive too:
[Reactive]
public IEnumerable<ToDoItem> Items { get; set; }
Then, you need to change the SetValues method to use object comparison, instead of traversing:
async Task SetValues()
{
Employee = _realm.Find<Assignee>(EmployeeId);
Title = Employee.Name;
Items = _realm.All<ToDoItem>().Where(x => x.Employee == Employee);
}
To sum up - you don't need to try and load the data in the ctor, since you don't know when the EmployeeId will be set. You are already tracking when the property will change and inside the SetValues command you simply need to change the expression predicate.

.NET Core SignalR: How to accomplish resource-based authorization?

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.

Find SignalR live connections

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.

How can I properly access my EF foreign key column straight after it has been added?

I have a basic notification system working on my site and I have started to make it work in real-time with SignalR but I'm having trouble accessing my foreign key to the user table.
I have the following code in one of my pages that generates a new Notification object
Notification NewNotification = new Notification
{
UserToId = u.UserId,
NotificationText = "some text"
};
db.Notifications.Add(NewNotification);
db.SaveChanges();
string name = NewNotification.UserFrom.DisplayName;
On the last line I get System.NullReferenceException: Object reference not set to an instance of an object
When I pass the NewNotification through SignalR to the client and try to access NewNotification.UserFrom.DisplayName the js console tells me that NewNotification.UserFrom is undefined. This happens despite me being able to access it on the layout page after the Notifications have been added.
The relevant parts of my model for my Notification is as follows
public class Notification
{
public Notification()
{
this.UserFromId = WebSecurity.CurrentUserId;
}
[Key]
public int NotificationId { get; set; }
public int UserToId { get; set; }
public int UserFromId { get; set; }
public virtual UserProfile UserTo { get; set; }
public virtual UserProfile UserFrom { get; set; }
[MaxLength]
public string NotificationText { get; set; }
}
And then my db context has the following to set up the foreign key relationship
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Notification>()
.HasRequired(n => n.UserFrom)
.WithMany(u => u.NotificationsFrom)
.HasForeignKey(n => n.UserFromId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Notification>()
.HasRequired(n => n.UserTo)
.WithMany(u => u.NotificationsTo)
.HasForeignKey(n => n.UserToId)
.WillCascadeOnDelete(false);
}
The question is:
How can I access UserFrom.DisplayName of the notification just after it's been created?
it is likely that your entity is not being refreshed after the save. you can either
1) reacquire it from the datastore with another query. If Notification has some sort of Identity column, it will be updated on SaveChanges() on your NewNotification object. That id can be used to retrieve the object. (Something like NewNotification = db.Notifications.Single(x => x.Id == NewNotification.Id)
2) If you hate the above option, you can do something like this:
((IObjectContextAdapter)db).ObjectContext.Refresh(RefreshMode.ClientWins, NewNotification); This will update your NewNotification object. Not pretty, but I ran into an issue similar to this recently and it worked for me.
3) since you have the userid in scope already, I would consider doing a db.Users.Single(x => x.UserId == u.UserId).DisplayName
If you want to access the User through the NewNotification object, I personally like option 2 because it becomes a bit clearer what your intentions are.... You want to refresh your NewNotifications object against the database and you want whatever is in the Client that doesn't match your object to "WIN".
I think option 3 is perhaps the cleanest, but its your call!

Mapping SignalR connections to users

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);
}
}

Resources