How to map a SignalR hub to a path with an argument - c#

I'm building a backend with Asp.Net Core & SignalR. The data are not themself located on the Asp.Net Core, but must be requested to some other devices, that will send the data to the Asp.Net Core server, and then the server must give them back to the client through the web socket.
I would like to react to the OnConnectedAsync to request my devices to send periodically some data to Asp.Net Core.
But this means that my hub must be able to know which data I need to be querying.
So I would like to have a Hub responding to the url /api/data/{id} and in my Hub, be able to know which ID has been requested.
app.MapHub<NotificationsHub>("/api/data/{id}");
The id identify a big group of data(that is requested and forwared as bulk), and multiple clients will request the same ID.
I can't find in the doc:
If this is possible
How should I specify the ID parameter?
How do I retrieve this ID in the hub?
If anybody has some pointer, it would be helpful, thank you very much

You can pass parameters as query string parameters. Pass the id from the connection url:
_hubConnection = new HubConnectionBuilder()
.WithUrl("/api/data?id=123")
.Build();
Then read it inside hub:
public override async Task OnConnectedAsync()
{
string id = Context.GetHttpContext().Request.Query["id"];
var connectionId = Context.ConnectionId;
await Groups.AddToGroupAsync(connectionId, id);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
string id = Context.GetHttpContext().Request.Query["id"];
var connectionId = Context.ConnectionId
await Groups.RemoveFromGroupAsync(connectionId, id);
await base.OnDisconnectedAsync(exception);
}
Send data:
_hubContext.Group("123").SomeMethod(dataBatch);
Map hub:
app.MapHub<NotificationsHub>("/api/data")

Related

C# Await till object is in list, for controller

Hi i currently have a web api system where i send a command to a Machine at another location. Id like to wait for the response from the system to be available to send back to the requester. Currently when a request is made i generate a UUID and once data comes back from the other location it matches with that UUID and adds it to a response list. Is there any way i can await that a list contains x
If not is there any other way that i might be able to transfer this object from the very distant class and thread to the controller for the response to be sent back?
Code from webrequest
[HttpGet]
[Route("Areas")]
public async Task<IHttpActionResult> GetArea(GetAreas getAreas)
{
OutScaleManager.LogRequest("Request to getAreas with ip: " + getAreas.IP + " i " + getAreas.startArea + " i " + getAreas.endArea);
foreach (NoxApiService noxService in OutScaleManager.nUtil.connectedInstance)
{
if (noxService.param.IPAddress == getAreas.IP)
{
noxService.GetAreas(getAreas.startArea, getAreas.endArea);
OutScaleManager.LogRequest("getAreas Sent, now moving to queue checks");
string UUID = OutScaleManager.nRAR.NoxRequestHandler(getAreas, RequestType.GETAREAS);
if (OutScaleManager.nRAR.GetAreasResponseSend)
{
await OutScaleManager.nRAR.ResponseList.ContainsKey(UUID);
}
//if (OutScaleManager.nRAR.GetAreasResponseSend) { return Ok(OutScaleManager.nRAR.NoxRequestHandler(getAreas, RequestType.GETAREAS)); }
}
}
OutScaleManager.LogRequest("This request was not sent, please create an instance and connected through the propper channels.");
return Ok("This request was not sent, please create an instance and connected through the propper channels.");
}
Hi i currently have a web api system where i send a command to a Machine at another location. Id like to wait for the response from the system to be available to send back to the requester. Currently when a request is made i generate a UUID and once data comes back from the other location it matches with that UUID and adds it to a response list.
Instead of using a list of available responses, structure your request/response system as a dictionary of outstanding requests. The dictionary would map GUIDs to TaskCompletionSource<T> where the T is the type of response data. For example, if your response data was TResponse, it could look like this:
private ConcurrentDictionary<Guid, TaskCompletionSource<TResponse>> _requests = new();
public Task<TResponse> GetAreasAsync(GetAreas getAreas)
{
var guid = OutScaleManager.nRAR.NoxRequestHandler(getAreas, RequestType.GETAREAS);
TaskCompletionSource<TResponse> tcs = new();
_requests.TryAdd(guid, tcs);
return tcs.Task;
}
// Called when a GetAreas request completes:
public void SaveAreasResponse(Guid requestId, TResponse data)
{
if (!_requests.TryRemove(requestId, out var tcs))
throw new InvalidOperationException("Response received for unknown request GUID.");
tcs.TrySetResult(data);
}

signalR with multiple client connections

I have written an Application where I am using SignalR. I am sending connectionId from Client to Server(controller).
Everything is working fine with single browser (request will sent to server with connectionId="conn_1") and signalR is sending response to only conn_1, but when i open new browser and send a request from that client the previous connection gets disposed. Which means only one connection with particular connectionId remains alive.
Is there any way SignalR can not dispose and send response to both with data they want?
I am new to SignalR and would really appropriate any help or guidance.
Angular SignalRService to start connection with server
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(this.paymentDraftHubUrl)
.build();
return this.hubConnection
.start()
.then(() => this.hubConnectionStatus = 'Connection started')
.catch(err => (this.hubConnectionStatus = 'Error while starting connection: ' + err));
}
sending connectionId from client component to Api
this.signalRService.startConnection().then((connection) => {
this.connectionId = connection.toString();
//Calling Api
this.getAllTransactionException(
this.connectionId,
this.pageNumber,
this.pageSize
}
MyHub class in C#
public class PaymentDraftServiceHub : Hub, IPaymentDraftHub
{}
Controller for API
using timer to keep calling repository for new data,
[HttpGet]
[Route("GetCsrTranactions")]
public IActionResult GetCsrTranactions([FromQuery] TransactionExceptionDataRequest queryParams)
{
TimeManager.Dispose();
var timerManager = new TimeManager(async () =>
await _paymentDraftHub.Clients.Clients.Client(queryParams.ConnectionId).SendAsync(SignalRConstants.TransferPaymentDraftServiceData, await _paymentTransactionRepository.GetCsrTranactionsAsync(queryParams)));
var response = new ResponseMessage { Message = "Accepted", Code = "201" };
return Ok(response);
}
Client can have multiple connections with multiple connection IDs if client connect from multiple browser windows or tabs.
According to the code you provided, we can find that you just pass connection ID of SignalR client within current active browser tab/window to your controller, and in your controller action, you use this code snippet .Client(queryParams.ConnectionId).SendAsync() to send message to a specific client, so other browser windows or tabs would not receive the message.
If you'd like to send message(s) to a client with multiple connections, you need to map SignalR users to connection Ids and retain information about users-to-connectionIds mapping, then you can get all connectionIds of a client and send messages to that client with with multiple connectionIds, like below.
//code logic here
//to get all connectinIds of a client/user
//from user-to-connectionIds mapping table
await _paymentDraftHub.Clients.Clients(connectionIds_here).SendAsync("method_here",args_here);

SignalR call to specific client from controller ASP.NET Core 2.1

I need to send an instant message from the server to the client after the user has submitted a form in a browser.
I followed the Microsoft steps here to set up a signalR connection, created a Hub class, signalr.js etc.
The problem is that I can only invoke a message to all clients, but I need to invoke the message to the specific caller who initiated the request (otherwise everyone will get the message).
This is my POST Action in the HomeController.cs:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Submit(string signalRconnectionId, Dictionary<string, string> inputs)
{
//Invoke signal to all clients sending a message to initSignal WORKS FINE
await _signalHubContext.Clients.All.SendAsync("initSignal", "This is a message from the server!");
//Invoke signal to specified client where signalRConnectionId = connection.id DOES NOT WORK
await _signalHubContext.Clients.Client(signalRconnectionId).SendAsync("initSignal", "This is a message from server to this client: " + signalRconnectionId);
return RedirectToAction("Success", inputs);
}
my client javascript file:
//Create connection and start it
const connection = new signalR.HubConnectionBuilder()
.withUrl("/signalHub")
.configureLogging(signalR.LogLevel.Information)
.build();
connection.start().catch(err => console.error(err.toString()));
console.log("connectionID: " + connection.id);
$("#signalRconnectionId").attr("value", connection.id);
//Signal method invoked from server
connection.on("initSignal", (message) => {
console.log("We got signal! and the message is: " + message);
});
I have tried debugging the action method and I get correctly passed in the connectionId which is "0" (incrementing by 1 per connection)
So here's the final solution I came up with inspired by the answer from this thread
I got the connectionId by calling the Hub class from client and then from client calling the controller passing in the connectionId.
Hub class:
public class SignalHub : Hub
{
public string GetConnectionId()
{
return Context.ConnectionId;
}
}
client-side javascript code executed at startup:
connection.start().catch(err => console.error(err.toString())).then(function(){
connection.invoke('getConnectionId')
.then(function (connectionId) {
// Send the connectionId to controller
console.log("connectionID: " + connectionId);
$("#signalRconnectionId").attr("value", connectionId);
});
});
HomeController.cs:
public async Task<IActionResult> Submit(string signalRconnectionId, Dictionary<string, string> inputs)
{
//Invoke signal to specified client WORKS NOW
await _signalHubContext.Clients.Client(signalRconnectionId).SendAsync("initSignal", "This is a message from server to this client: " + signalRconnectionId);
return RedirectToAction("Success", inputs);
}
It works fine, but still feels a little like a roundtrip, it would have been easier if we didn't have to go through the hub class to make this happen. Maybe just having the connectionId from the client-side to begin with, but maybe there is a good reason for the design :)
According to Microsoft, you can not access to the ConnectionId and Caller from outside a hub
https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext?view=aspnetcore-2.1
When hub methods are called from outside of the Hub class, there's no
caller associated with the invocation. Therefore, there's no access to
the ConnectionId, Caller, and Others properties.

.Net Core SignalR: Send to Users except caller from IHubContext (injected into controller)

in order to identify the current user only when working with an injected IHubContext in a controller, I am storing a group with the user id.
However, I'm struggling to send to everyone else since I cannot figure out a way to find out which connection ID to exclude.
My Hub
public override Task OnConnectedAsync()
{
Groups.AddAsync(Context.ConnectionId, Context.User.Identity.Name);
return base.OnConnectedAsync();
}
In my controller method, I can invoke methods for that user:
await _signalRHub.Clients.Group(User.Identity.Name).InvokeAsync("Send", User.Identity.Name + ": Message for you");
IHubContext.Clients.AllExcept requires a list of connection IDs. How can I obtain the connection ID for the identified user in order to only notify others?
As suggested by #Pawel, I am now de-duping on the client, which works (well, as long as all your clients are authenticated).
private async Task Identification() => await Clients.Group(Context.User.Identity.Name).InvokeAsync("Identification", Context.User.Identity.Name);
public override async Task OnConnectedAsync()
{
await Groups.AddAsync(Context.ConnectionId, Context.User.Identity.Name);
await base.OnConnectedAsync();
await Identification();
}
The JS to go along with it (abbreviated):
var connection = new signalR.HubConnection("/theHub");
var myIdentification;
connection.on("Identification", userId => {
myIdentification = userId;
});
Now you can test for callerIdentification == myIdentification in addtional methods like connection.on("something", callerIdentification)
#Tester's comment makes me hopeful there'll be a better way at some point when sending through IHubContext.
In SignalR core, the connectionId is stored in the signalR connection. Assuming you have a signalrR connection defined as follows
signalrConnection = new HubConnectionBuilder()
.withUrl('/api/apphub')
...
.build();
Whenever you make a fetch request, add the signalR connectionId as a header. Eg.
response = await fetch(url, {
...
headers: {
'x-signalr-connection': signalrConnection.connectionId,
},
});
Then in your controller or wherever you have access to a httpContextAccessor, you can use the following to exclude the connection referenced in the header:
public async Task NotifyUnitSubscribersExceptCaller()
{
//Grab callers connectionId
var connectionId = _httpContextAccessor.HttpContext?.Request.Headers["x-signalr-connection"] ?? "";
await _hub.Clients.GroupExcept("myGroup", connectionId).SendCoreAsync("Sample", new object[] { "Hello World!" });
}

Can't send message to specific user with SignalR

I can't make works the message sending to one specific user from the code behind. Clients.All works, Clients.AllExcept(userId) works, but not Client.User(userId).
My hub:
public class MessagingHub : Hub
{
public override Task OnConnected()
{
var signalRConnectionId = Context.ConnectionId;
// for testing purpose, I collect the userId from the VS Debug window
System.Diagnostics.Debug.WriteLine("OnConnected --> " + signalRConnectionId);
return base.OnConnected();
}
}
My controller to send message from code behind:
public void PostMessageToUser(string ConnectionId)
{
var mappingHub = GlobalHost.ConnectionManager.GetHubContext<MessagingHub>();
// doesn't works
mappingHub.Clients.User(ConnectionId).onMessageRecorded();
// doesn't works
mappingHub.Clients.Users(new List<string>() { ConnectionId }).onMessageRecorded();
// works
mappingHub.Clients.All.onMessageRecorded();
// works (?!)
mappingHub.Clients.AllExcept(ConnectionId).onMessageRecorded();
}
How my hub is initialized on the JS:
var con, hub;
function StartRealtimeMessaging()
{
con = $.hubConnection();
hub = con.createHubProxy('MessagingHub');
hub.on('onMessageRecorded', function () {
$(".MessageContainer").append("<div>I've received a message!!</div>");
});
con.start();
}
And finally how I send a(n empty) message to the hub:
function TestSendToUser(connectionId)
{
$.ajax({
url: '/Default/PostMessageToUser',
type: "POST",
data: { ConnectionId: connectionId},// contains the user I want to send the message to
});
}
So, it works perfectly with mappingHub.Clients.All.onMessageRecorded(); but not with mappingHub.Clients.User(ConnectionId).onMessageRecorded(); or mappingHub.Clients.Users(new List<string>() { ConnectionId}).onMessageRecorded();.
But interestingly, it works with mappingHub.Clients.AllExcept(ConnectionId).onMessageRecorded(); : All users connected receive the message except the given userid, which means the userid is good, and the user is well identified. So, why Clients.User(ConnectionId) doesn't works?
If you want to send a message to one particular connection and when you want to use the ConnectionId, make sure you use Clients.Client, and not Clients.User
Like this:
public void PostMessageToUser(string connectionId)
{
var mappingHub = GlobalHost.ConnectionManager.GetHubContext<MessagingHub>();
// Like this
mappingHub.Clients.Client(connectionId).onMessageRecorded();
// or this
mappingHub.Clients.Clients(new List<string>() { connectionId }).onMessageRecorded();
}
I had the same problem. I couldn't get .User(ConnectionId) to work.
I have just spent days trying to get SignalR to report progress on a long processing job to only the client who requested the job. That is, it isn't a chat app which most of the examples describe.
Any 'long processing progress reporting' examples I found only have a sim of the job in the hub. I have a controller doing real work and need to send messages from the controller, not the hub.
I used this answer https://stackoverflow.com/a/21222303/3251300. as a workaround for your stated problem but have included all the code snippets I use for the long processing job in case they are useful for anyone who stumbles on this answer.
The workaround has an elegance in that it uses the .Group() feature. By setting each groupID equal to the internal userID, messages can be sent using .Group(userID) without having to separately maintain a list of the userID/connectionID relationships outside SignalR.
There may be a way to maintain the relationships in SignalR without using the .Group() feature but I haven’t found it yet.
Pass the userID to the view using a hidden type which then makes it available to the js.
<input type="hidden" value="#ViewBag.UserID" id="userID" />
Then in the js hub script use the following to send the userID to the hub when the hub connection starts up.
$.connection.hub.start()
.done(function () {
var userID = document.getElementById('userID').value;
$.connection.myHub.server.announce(userID);
})
.fail(function () { alert("Hub failed to start.") });
The hub then has one statement which associates the userID and connectionID to the groupID, which is then the same string as the userID.
public class MyHub : Hub
{
public void Announce(string userID)
{
Groups.Add(Context.ConnectionId, userID);
}
}
To send messages from the controller (Again, not the hub in this case, the message is reporting progress to the client on a long processing request running in the controller) after setting the hub context, use .Group() and the internal userID.
var hubContext = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
string fileMessage = "Some message";
hubContext.Clients.Group(userID).hubMessage(fileMessage);
This is then displayed in the view using the js to place the message in a div
$.connection.myHub.client.hubMessage = function (message) {
$("#hubMessages").html(message);
}
'#hubMessages' refers to this div in the view. Examples use .append which makes the div grow each time you send a message, .HTML replaces whatever is in the div with the new message.
<div id="hubMessages"></div>
Anyone who comes to this answer and is trying to get going on MVC and SignalR, a big shout out to Caleb who has a great series of intro vids for SignalR https://youtu.be/kr8uHeNjOKw Anyone who finds this answer who is new to SignalR I recommend you spend an hour watching these.
I face same problem.
I change from:
Clients.User(connectionId).SendAsync(CallbackDefinition.DirectMessage, directMessageResult);
to:
Clients.Client(connectionId).SendAsync(CallbackDefinition.DirectMessage, directMessageResult);
And it work :D
Thank to: Matthieu Charbonnier

Categories

Resources