I want to allow my users connect to a specific group in my SignalR hub, to do this i generate a unique id that the users in that group can share with others. Once a user connects to the hub, the id is generated. On the "connected" event their URL updates with the unique id. When I then use the URL to join the newly created room it seems like two negotiation requests are sent, both containing the the group id as well as the users connection id, yet sometimes(not always) I get a response from the Hub containing a newly generated Group.
Is the ?pod parameter I pass into the url not always assigned before the request is made?
To me it seems completely random, but it's most likely some error I've made in my connection code since I'm relatively new to Angular.
This request happened correctly and I joined the room I wanted.
Correct behavior
This one happened incorrectly and a new room was generated even though, seemingly(?), the request looks the same, save for the web socket connection containing the "pid".
Incorrect behavior
Any help is greatly appreciated!
The code for the Home component where the connection is initiated
export class HomeComponent implements OnInit, OnDestroy{
welcomeMessage:string;
podId:string;
users:User[];
constructor(private signalrService: SignalRService, private http: HttpClient, private activeRoute: ActivatedRoute,
private router: Router, private location: Location) {}
ngOnInit() {
this.activeRoute.queryParams.subscribe(params => {
this.podId = params['pod'];
this.connectToPod();
});
}
ngOnDestroy(): void {
}
connectToPod(){
this.signalrService.startConnection(this.podId);
this.signalrService.addPodConnectedLisener((data: Pod) => {
this.podId = data.id;
this.users = data.users;
this.location.replaceState('/?pod=' + this.podId)
this.welcomeMessage = window.location.origin + '/?pod=' + this.podId;
});
}
}
The code for the SignalR service
export class SignalRService {
private hubConnection: signalR.HubConnection;
private baseUrl = environment.apiUrl;
constructor() { }
public startConnection (podId?: string) {
let idToSend = podId == undefined ? '' : '?pid=' + podId;
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(this.baseUrl + '/pod' + idToSend)
.build();
this.hubConnection
.start()
.then(() => console.log('Connection started'))
.catch(err => console.log('Error while starting connection: ' + err));
}
public addPodConnectedLisener (connectedCallback: Function) {
return this.hubConnection.on('connected', data => {
connectedCallback(data);
});
}
}
The code for the SignalR Hub
public class PodHub : Hub
{
private readonly IPodConnectionManger _podConnectionManager;
public PodHub(IPodConnectionManger podConnectionManager)
{
_podConnectionManager = podConnectionManager;
}
public override async Task OnConnectedAsync()
{
var podId = Context.GetHttpContext().Request.Query["pid"].ToString();
if (string.IsNullOrEmpty(podId))
{
await CreatePod();
}
else
{
await JoinPod(podId);
}
}
private async Task CreatePod()
{
var newPodId = await _podConnectionManager.AddPod();
var podToSend = await _podConnectionManager.GetPod(newPodId);
await podToSend.AddUser(Context.ConnectionId);
await Groups.AddToGroupAsync(Context.ConnectionId, podToSend.Id);
await Clients.Group(podToSend.Id).SendAsync("connected", podToSend);
}
private async Task JoinPod(string id)
{
var podToJoin = await _podConnectionManager.GetPod(id);
await podToJoin.AddUser(Context.ConnectionId);
await Groups.AddToGroupAsync(Context.ConnectionId, podToJoin.Id);
await Clients.Group(podToJoin.Id).SendAsync("connected", podToJoin);
}
}
Related
I want to know if there is a way to read data changes on MySQL with signal R. I saw that you can do this on SQL with SQL dependency. My objective is to do this to display notifications on ASP .Net Core. This is what I have:
Hub
public class NotificationsHub : Hub
{
private readonly INotificationsUserService _notificationsUserService;
private readonly IMapper _mapper;
public NotificationsHub(INotificationsUserService notificationsUserService, IMapper mapper)
{
_notificationsUserService= notificationsUserService;
_mapper = mapper;
}
public async Task GetNotifications(int userId)
{
var notifications = await _notificationsUserService.GetAllNotificationUsers();
notifications = notifications .Where(n => n.IdUser == userId);
var userNotifications = new List<Notifications>();
foreach (var item in notifications )
{
userNotifications.Add(item.IdNotificationsNavigation);
}
var notificationResource = _mapper.Map<IEnumerable<Notifications>, IEnumerable<NotificationsResource>>(userNotifications);
await Clients.All.SendAsync("usersNotifications", notificationResource);
}
}
Js
$(() => {
let connection = new signalR.HubConnectionBuilder().withUrl("/notificationsHub").build();
let userId = $('#userId').html();
setInterval(function () {
connection.start().then(() => {
connection.invoke("GetNotificationes", parseInt(userId)).catch(function (err) {
return console.error(err.toString());
});
});
}, 5000);
var notificaciones =
connection.on("usersNotifications", function (notifications) {
$.ajax({
type: "POST",
url: "/Home/RefreshNavComponent",
data: {
"notifications": notifications
},
success: function (data) {
$('#header').html(data);
connection.stop();
},
error: function (req, status, error) {
alert(req + " " + status + " " + error);
console.log(req);
console.log(status);
}
});
});
Home Controller
[HttpPost]
public IActionResult RefreshNavComponent(IEnumerable<NotificationsResource> notifications)
{
if (notifications != null)
{
return ViewComponent("NavMenu", notifications);
}
return Ok();
}
As you can see, currently im updating every 5 seconds my Nav where the notifications icon is. But I dont want it to update the full Nav. And I think calling a timer defeats the purpose of Signal R. Let me know if you have any suggestions.
Not seeing it in your code, so assuming that you have a method that updates the notifications table/record in the database. You can update your clients by using a Client.All or Clients.User(user) or Clients.Group(groupName) in the same method but after the update.
If you do this from a controller and not the Hub, you will want to check out how to
send from outside the hub.
I am new to gRPC and trying to learn it by using the chat server/client sample from cactuaroid here. I’ve modified the code to show progress in a WPF app from a long running task. All code is running on .NET 5 and I’m using the latest versions of the gRPC packages.
The process is working fine when using the computer's IP address but when using computer name for the gRPC client, I’m getting a “DNS resolution failed” exception (computer name is “skylake”):
RpcException: Status(StatusCode="Unavailable", Detail="DNS resolution
failed for service: skylake:6001",
DebugException="Grpc.Core.Internal.CoreErrorDetailException:
{"created":"#1615312867.300000000","description":"Resolver transient
failure","file":"......\src\core\ext\filters\client_channel\client_channel.cc","file_line":2138,"referenced_errors":[{"created":"#1615312867.300000000","description":"DNS
resolution failed for service:
skylake:6001","file":"......\src\core\ext\filters\client_channel\resolver\dns\c_ares\dns_resolver_ares.cc","file_line":362,"grpc_status":14,"referenced_errors":[{"created":"#1615312867.300000000","description":"C-ares
status is not ARES_SUCCESS qtype=AAAA name=skylake is_balancer=0:
Could not contact DNS
servers","file":"......\src\core\ext\filters\client_channel\resolver\dns\c_ares\grpc_ares_wrapper.cc","file_line":716,"referenced_errors":[{"created":"#1615312866.142000000","description":"C-ares
status is not ARES_SUCCESS qtype=A name=skylake is_balancer=0: Could
not contact DNS
servers","file":"......\src\core\ext\filters\client_channel\resolver\dns\c_ares\grpc_ares_wrapper.cc","file_line":716}]}]}]}")
I verified that I could reach the port with telnet skylake 6001.
I am testing locally, client and server both on the same machine. Oddly enough, the gRPC server seems to be just fine with the computer name. Its just the client that has an issue with it.
Server code:
[Export(typeof(IService))]
public class ProgressServiceGrpcServer : Progress.ProgressBase, IService
{
[Import]
private Logger m_logger = null;
[Import]
private ProgressService m_progressService = null;
private readonly Empty m_empty = new Empty();
private const int Port = 6001;
private readonly Grpc.Core.Server m_server;
public ProgressServiceGrpcServer()
{
m_server = new Grpc.Core.Server
{
Services =
{
Progress.BindService(this)
.Intercept(new IpAddressAuthenticator())
},
Ports =
{
new ServerPort("skylake", Port, ServerCredentials.Insecure)
}
};
}
public void Start()
{
m_server.Start();
m_logger.Info("Started.");
}
public override async Task Subscribe(ChannelName channelName, IServerStreamWriter<ProgressReport> responseStream, ServerCallContext context)
{
context.CancellationToken.Register(() => m_logger.Info($"{context.Host} cancels subscription."));
try
{
await m_progressService.GetProgressReportsAsObservable(channelName)
.ToAsyncEnumerable()
.ForEachAwaitAsync(async (x) => await responseStream.WriteAsync(x), context.CancellationToken)
.ConfigureAwait(false);
}
catch (TaskCanceledException)
{
m_logger.Info($"{context.Host} unsubscribed.");
}
}
public override Task<Empty> Write(ProgressReport request, ServerCallContext context)
{
m_logger.Info($"{context.Host} {request}");
m_progressService.Add(request);
return Task.FromResult(m_empty);
}
}
Client code:
public class ProgressServiceClient
{
private readonly Progress.ProgressClient m_client =
new Progress.ProgressClient(
new Channel("skylake”, 6001, ChannelCredentials.Insecure));
public async Task Write(ProgressReport progressReport)
{
await m_client.WriteAsync(progressReport);
}
public IAsyncEnumerable<ProgressReport> ProgressReports(ChannelName channelName)
{
var call = m_client.Subscribe(channelName);
return call.ResponseStream
.ToAsyncEnumerable()
.Finally(() => call.Dispose());
}
}
Progress write method:
while (inProgress)
{
progressServiceClient.Write(new GrpcServer.ProgressReport
{
Id = Task.Id.ToString(),
PercentDone = percentDone,
TimeRemain = timeRemain
}).Wait();
Thread.Sleep(500);
}
Progress read method:
m_progressService = new ProgressServiceClient();
ChannelName channelName = new ChannelName() { Id = id };
var cts = new CancellationTokenSource();
_ = m_progressService.ProgressReports(channelName)
.ForEachAsync((x) =>
{
Log.Debug($"id: {x.Id} progress: {x.PercentDone}");
}, cts.Token);
this.Dispatcher.Invoke(() =>
{
Application.Current.Exit += (_, __) => cts.Cancel();
this.Unloaded += (_, __) => cts.Cancel();
});
Thanks to #jdweng for pointing me in the right direction, this was solved by adding the DNS suffix to the hostname (skylake.lan in my case).
We can get the DNS suffix via IPInterfaceProperties.DnsSuffix.
Alternatively, (which might be safer) we can use the correct IP address instead of the host name by using something like that.
I've been driving myself nuts trying to resolve this issue so really hoping someone has some insight.
I have a console application which runs/hosts my signalR server.
I have already successfully connected to it using a web(javascript) client and a windows forms client with no trouble at all.
BUT for the life of me I cannot get a silverlight client to connect to it. Initially I was getting a
'System.Security.SecurityException' occurred in Microsoft.Threading.Tasks error
on
await Connection.Start();
I managed to fix that by force sending the clientaccesspolicy file using code i found on a random thread.
THREAD
However the connection still never establishes. The status goes thru connecting, disconnected, connection closed.
I am at my wits end as to why this won't work. Any input is appreciated. Code below.
MainPage.xaml.cs
public partial class MainPage : UserControl
{
private SignalRClient client;
public MainPage()
{
InitializeComponent();
dataGrid1.ItemsSource = new ItemsCollection();
client = new SignalRClient();
client.RunAsync();
Debug.WriteLine("Init Done");
}
}
-
SignalRClient.cs
public class SignalRClient
{
private HubConnection Connection { get; set; }
private IHubProxy HubProxy { get; set; }
const string url = "http://localhost:8080/";
public SignalRClient()
{
}
public async void RunAsync()
{
Connection = new HubConnection(url, useDefaultUrl: true);
Connection.Closed += Connection_Closed;
Connection.StateChanged += ConnectionDidSomething;
HubProxy = Connection.CreateHubProxy("TickerHub");
HubProxy.On<string>("receiveAllData", data => Debug.WriteLine("RECDATA={0}", data));
try
{
await Connection.Start();
}
catch (HttpClientException e)
{
Debug.WriteLine("Unable to connect to server.1 {0}", e.Message);
return;
}
catch (HttpRequestException e)
{
Debug.WriteLine("Unable to connect to server.2 {0}", e.Message);
return;
}
}
-
Server
class Program
{
static void Main(string[] args)
{
string url = "http://localhost:8080/";
using (WebApp.Start(url))
{
Console.WriteLine("SignalR server running on {0}", url);
Console.ReadLine();
}
Console.ReadLine();
}
}
class Startup
{
public void Configuration(IAppBuilder app)
{
Console.WriteLine("Configuration");
//Tried this approach too
/*app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
EnableJSONP = true
};
map.RunSignalR(hubConfiguration);
});*/
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR<ClientAccessPolicyConnection>("/clientaccesspolicy.xml");
}
}
-
TickerHub.cs
public class TickerHub : Hub
{
public override Task OnConnected()
{
string connectionID = Context.ConnectionId;
Console.WriteLine("New Connection:" + connectionID);
InitNewClient(connectionID);
return base.OnConnected();
}
//send all data to newly connected client
public void InitNewClient(string connectionID)
{
}
//client requested all data
public void GetAllData()
{
Console.WriteLine("Get Data Triggered");
Clients.All.receiveAllData("TESTING123");
}
}
I figured it out! Hopefully this helps someone in the future.
Its quite simple. This is what you need to have in your startup class configuration method.
Below that is the code required to send the clientaccesspolicy.xml.
class Startup
{
public void Configuration(IAppBuilder app)
{
// Branch the pipeline here for requests that start with "/signalr"
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
// You can enable JSONP by uncommenting line below.
// JSONP requests are insecure but some older browsers (and some
// versions of IE) require JSONP to work cross domain
EnableJSONP = true
};
// Run the SignalR pipeline. We're not using MapSignalR
// since this branch already runs under the "/signalr"
// path.
map.RunSignalR(hubConfiguration);
});
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR<ClientAccessPolicyConnection>("/clientaccesspolicy.xml");
}
}
-
public class ClientAccessPolicyConnection : PersistentConnection
{
public override Task ProcessRequest(Microsoft.AspNet.SignalR.Hosting.HostContext context)
{
string[] urlArray = context.Request.Url.ToString().Split('/');
string path = urlArray[urlArray.Length - 1];
if (path.Equals("clientaccesspolicy.xml", StringComparison.InvariantCultureIgnoreCase))
{
//Convert policy to byteArray
var array = Encoding.UTF8.GetBytes(ClientAccessPolicy);
var segment = new ArraySegment<byte>(array);
//Write response
context.Response.ContentType = "text/xml";
context.Response.Write(segment);
//Return empty task to escape from SignalR's default Connection/Transport checks.
return EmptyTask;
}
return EmptyTask;
}
private static readonly Task EmptyTask = MakeTask<object>(null);
public static Task<T> MakeTask<T>(T value)
{
var tcs = new TaskCompletionSource<T>();
tcs.SetResult(value);
return tcs.Task;
}
public static readonly string ClientAccessPolicy =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<access-policy>"
+ "<cross-domain-access>"
+ "<policy>"
+ "<allow-from http-request-headers=\"*\">"
+ "<domain uri=\"*\"/>"
+ "</allow-from>"
+ "<grant-to>"
+ "<resource path=\"/\" include-subpaths=\"true\"/>"
+ "</grant-to>"
+ "</policy>"
+ "</cross-domain-access>"
+ "</access-policy>";
}
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
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);
}
}
}