SignalR's negotiate calls returns a connection id. Is there a way to get connection id on server side when negotiate request has been called?
Is there a server side event that I can subscribe to when connection id has been generated?
If you are using a PersistentConnection you can override the
ProcessRequestAsync(HostContext context)
method and get the connectionId value from the IResponse property of the HostContext.
Another way would be to use the DependecyResolver to provide your own implementation of IConnectionIdFactory that raises an event before returning the connectionId string.
If you are using SignalR Hub, you can listen to the connect event then grab Context.ConnectionId. E.g.
public override Task OnConnected()
{
return Clients.All.joined(Context.ConnectionId, DateTime.Now.ToString());
}
See https://github.com/SignalR/SignalR/wiki/Hubs for more.
string connectionID = Context.ConnectionId;
Related
Hello I'm developing a Server-Client application that communicate with SignalR. What I have to implement is a mechanism that will allow my server to call method on client and get a result of that call. Both applications are developed with .Net Core.
My concept is, Server invokes a method on Client providing Id of that invocation, the client executes the method and in response calls the method on the Server with method result and provided Id so the Server can match the Invocation with the result.
Usage is looking like this:
var invocationResult = await Clients
.Client(connectionId)
.GetName(id)
.AwaitInvocationResult<string>(ClientInvocationHelper._invocationResults, id);
AwaitInvocationResult - is a extension method to Task
public static Task<TResultType> AwaitInvocationResult<TResultType>(this Task invoke, ConcurrentDictionary<string, object> lookupDirectory, InvocationId id)
{
return Task.Run(() =>
{
while (!ClientInvocationHelper._invocationResults.ContainsKey(id.Value)
|| ClientInvocationHelper._invocationResults[id.Value] == null)
{
Thread.Sleep(500);
}
try
{
object data;
var stingifyData = lookupDirectory[id.Value].ToString();
//First we should check if invocation response contains exception
if (IsClientInvocationException(stingifyData, out ClientInvocationException exception))
{
throw exception;
}
if (typeof(TResultType) == typeof(string))
{
data = lookupDirectory[id.Value].ToString();
}
else
{
data = JsonConvert.DeserializeObject<TResultType>(stingifyData);
}
var result = (TResultType)data;
return Task.FromResult(result);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
});
}
As you can see basically I have a dictionary where key is invocation Id and value is a result of that invocation that the client can report. In a while loop I'm checking if the result is already available for server to consume, if it is, the result is converted to specific type.
This mechanism is working pretty well but I'm observing weird behaviour that I don't understand.
If I call this method with await modifier the method in Hub that is responsible to receive a result from client is never invoked.
///This method gets called by the client to return a value of specific invocation
public Task OnInvocationResult(InvocationId invocationId, object data)
{
ClientInvocationHelper._invocationResults[invocationId.Value] = data;
return Task.CompletedTask;
}
In result the while loop of AwaitInvocationResult never ends and the Hub is blocked.
Maby someone can explain this behaviour to me so I can change my approach or improve my code.
As it was mentioned in the answer by Brennan, before ASP.NET Core 5.0 SignalR connection was only able to handle one not streaming invocation of hub method at time. And since your invocation was blocked, server wasn't able to handle next invocation.
But in this case you probably can try to handle client responses in separate hub like below.
public class InvocationResultHandlerHub : Hub
{
public Task HandleResult(int invocationId, string result)
{
InvoctionHelper.SetResult(invocationId, result);
return Task.CompletedTask;
}
}
While hub method invocation is blocked, no other hub methods can be invoked by caller connection. But since client have separate connection for each hub, he will be able to invoke methods on other hubs. Probably not the best way, because client won't be able to reach first hub until response will be posted.
Other way you can try is streaming invocations. Currently SignalR doesn't await them to handle next message, so server will handle invocations and other messages between streaming calls.
You can check this behavior here in Invoke method, invocation isn't awaited when it is stream
https://github.com/dotnet/aspnetcore/blob/c8994712d8c3c982111e4f1a09061998a81d68aa/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs#L371
So you can try to add some dummy streaming parameter that you will not use:
public async Task TriggerRequestWithResult(string resultToSend, IAsyncEnumerable<int> stream)
{
var invocationId = InvoctionHelper.ResolveInvocationId();
await Clients.Caller.SendAsync("returnProvidedString", invocationId, resultToSend);
var result = await InvoctionHelper.ActiveWaitForInvocationResult<string>(invocationId);
Debug.WriteLine(result);
}
and on the client side you will also need to create and populate this parameter:
var stringResult = document.getElementById("syncCallString").value;
var dummySubject = new signalR.Subject();
resultsConnection.invoke("TriggerRequestWithResult", stringResult, dummySubject);
dummySubject.complete();
More details: https://learn.microsoft.com/en-us/aspnet/core/signalr/streaming?view=aspnetcore-5.0
If you can use ASP.NET Core 5, you can try to use new MaximumParallelInvocationsPerClient hub option. It will allow several invocations to execute in parallel for one connection. But if your client will call too much hub methods without providing result, connection will hang.
More details: https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-5.0&tabs=dotnet
Actually, since returning values from client invocations isn't implemented by SignalR, maybe you can try to look into streams to return values into hubs?
This is supported in .NET 7 now https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-7.0#client-results
By default a client can only have one hub method running at a time on the server. This means that when you wait for a result in the first hub method, the second hub method will never run since the first hub method is blocking the processing loop.
It would be better if the OnInvocationResult method ran the logic in your AwaitInvocationResult extension and the first hub method just registers the id and calls the client.
We have a Service Bus queue that handles multiple message topics/subscriptions and what we'd like to be able to do is when certain messages have been handled is to notify connected users that a message has been handled.
The message handling takes place in a simple console app but we're not sure how to create a connection to our Azure SignalR service and send a message once it's been processed.
I believe the simplest most scalable approach would be to have a simple azure function to do this.
You would just have to use the Service Bus Trigger which runs your function when a message arrives and use the SignalR Service Output Binding to send the message to your users.
Your function could be as simple as the following
[FunctionName("ServiceBusQueueTriggerCSharp")]
public static void Run(
[ServiceBusTrigger("myqueue", AccessRights.Manage, Connection = "ServiceBusConnection")]
string myQueueItem,
[SignalR(HubName = "chat")]IAsyncCollector<SignalRMessage> signalRMessages
ILogger log)
{
return signalRMessages.AddAsync(
new SignalRMessage
{
Target = "newMessage",
Arguments = new [] { myQueueItem }
});
}
So I am fairly new with signalR and had worked with it a bit with MVCs. Now I am using it in webapi with angularjs and am a bit confused or have forgotten of what I have done. I am using bearer tokens with webapi and am trying to create a notification system.
What I want to figure out is the proper way of using angularjs with signalR. I see many people use the proxy on/invoke. Is the proxy.on is when I call the hubcontext from the server as so:
IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();
hubContext.Clients.User(UserId).broadcastNotification("Good morning! The time is " + DateTime.Now.ToString());
and the proxy.invoke method is from the client side? If so, which would be the best way for using notification systems (I would assume the proxy.on)?
My second question is more on sending notifications to specific users. For sending requests to specific users, I would assume you would want to do this on the hub as so:
public void SendNotification(string userId)
{
Clients.User(userId).broadcastNotification("Good morning! The time is " + DateTime.Now.ToString());
}
My startup is something like this:
public class MyProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
var userId = request.User.Identity.Name;
return userId.ToString();
}
}
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
Database.SetInitializer(new MigrateDatabaseToLatestVersion<AuthContext, Travelfy.API.Migrations.Configuration>());
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => new MyProvider());
app.MapSignalR("/hubs", new HubConfiguration());
}
When I refresh my pages, I notice that all my userids are all empty strings "". I was reading that maybe it was due to using bearer tokens. If so, how would I use bearer tokens to specific the userId that I would want to send to? When I use the Clients.All everything works fine, so I'm assuming it has to be something with the startup/userIds I am getting.
Thanks
To answer your first question:
Which would be the best way for using notification systems
If you want to push notifications from the server towards the client, you have to define a new handler is to define a method on the client (with the generated proxy) like this:
How to define methods on the client that the server can call
If you want the client to call a method that lies on the server, you have to use this method:
How to call server methods from the client
To answer your second question:
For sending requests to specific users, I would assume you would want
to do this on the hub
You could use the connection ID of the client you wish to target. See this:
Calling client methods
So after a while, I was able to figure out the right answer. Because I was using bearerTokens, I really had to determine another method of obtaining the userId rather than just relying on request.User.Identity.Name. What I needed to do was pass my bearerToken to the connection.qs value.
connection.qs = { Bearer: token };
Once I was able to do that I had to route the find my user based on the token that I had sent in.
var token = request.QueryString.Get("Bearer");
var authenticationTicket = Startup.OAuthBearerOptions.AccessTokenFormat.Unprotect(token);
I have setup a SignalR hub which has the following method:
public void SomeFunction(int SomeID)
{
try
{
Thread.Sleep(600000);
Clients.Caller.sendComplete("Complete");
}
catch (Exception ex)
{
// Exception Handling
}
finally
{
// Some Actions
}
m_Logger.Trace("*****Trying To Exit*****");
}
The issue I am having is that SignalR initiates and defaults to Server Sent Events and then hangs. Even though the function/method exits minutes later (10 minutes) the method is initiated again ( > 3 minutes) even when the sendComplete and hub.stop() methods are initiated/called on the client prior. Should the user stay on the page the initial "/send?" request stays open indefinitely. Any assistance is greatly appreciated.
To avoid blocking the method for so long, you could use a Taskand call the client method asynchronously.
public void SomeFunction(Int32 id)
{
var connectionId = this.Context.ConnectionId;
Task.Delay(600000).ContinueWith(t =>
{
var message = String.Format("The operation has completed. The ID was: {0}.", id);
var context = GlobalHost.ConnectionManager.GetHubContext<SomeHub>();
context.Clients.Client(connectionId).SendComplete(message);
});
}
Hubs are created when request arrives and destroyed after response is sent down the wire, so in the continuation task, you need to create a new context for yourself to be able to work with a client by their connection identifier, since the original hub instance will no longer be around to provide you with the Clients method.
Also note that you can leverage the nicer syntax that uses async and await keywords for describing asynchronous program flow. See examples at The ASP.NET Site's SignalR Hubs API Guide.
I received data from different server to my Hub class. Each data has its own ID. Whenever data comes to the server hub, it push my data to the client. This is like job progress.
I want to send each ID to the client with unique hub id., How do I filter the message from the server? I used in this way Clients.Client("ID1").send(data); Or I have to specify in caller property? Anyone can help me.
With Regards,
Shanthini
You can use ConnectionId to identify the client.
When new client is connected, store ConnectionId somewhere so that you can use it later to identify the client.
public class MyHub : Hub
{
public override Task OnConnected()
{
var connectionId = Context.ConnectionId;
// store connectionId somewhere
return base.OnConnected();
}
}
To send data to the client, identify it by ConnectionId:
public void SendNewData(string connectionId, object data)
{
var Context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
Context.Clients.Client(connectionId).send(data);
}
If you need to identify clients by some other ID, then you should store relationship between your ID and ConnectionId.