All the documentation I've seen so far show how to map connection ids to users via the Hub's OnConnected
/OnDisconnected
methods. But how can I do this in a Serverless setup? Is it at all possible in Azure Functions?
I have implemented the negotiate
endpoint, so I have access to SignalRConnectionInfo
. I have a Send method with a SignalR
output binding, to which I can add an instance of SignalRMessage
, which then gets broadcast to all connected clients. The SignalRMessage
class does have UserId
and GroupName
properties, so I believe it must be possible to specify a user or a group to send the message to.
If you are not using Azure App Service built-in authentication and authorization support you can send the user id (unique user identification) inside the header.
[FunctionName("negotiate")]
public static SignalRConnectionInfo GetSignalRInfo(
[HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req,
[SignalRConnectionInfo(HubName = "tyton", UserId = "{headers.x-ms-signalr-userid}")] SignalRConnectionInfo connectionInfo,
ILogger log)
{
// Provide your own authentication
// If authenticated return connection informations
return connectionInfo;
}
Note:- The header is different x-ms-signalr-userid
And your client (I have used Typescript) should do an HTTP POST call to get the connection information from the negotiate function and then build the signalR hub connection with the connection information received from the negotiation HTTP call.
import axios from "axios";
const apiBaseUrl = `http://localhost:7071`;
let connection: signalR.HubConnection;
function connect(userId: string) {
// Negotiation call
getConnectionInfo(userId).then(
info => {
// make compatible with old and new SignalRConnectionInfo
info.accessToken = info.accessToken || info.accessKey;
info.url = info.url || info.endpoint;
const options = {
accessTokenFactory: () => info.accessToken
};
// Create hub connection
connection = new signalR.HubConnectionBuilder()
.withUrl(info.url, options)
.configureLogging(signalR.LogLevel.Information)
.configureLogging(signalR.LogLevel.Error)
.build();
connection.on('newMessage', newMessage);
connection.on('newConnection', newConnection);
connection.onclose(() => console.log('disconnected'));
console.log('connecting...');
console.log(userId);
connection.start()
.then((data: any) => {
console.log("connected");
})
.catch((err: any) => {
console.log("Error");
});
})
.catch(err => console.log(err));
}
function getConnectionInfo(userId: string) {
return axios.post(`${apiBaseUrl}/api/negotiate`, null, getAxiosConfig(userId))
.then(resp => resp.data);
}
function getAxiosConfig(userId: string) {
const config = {
headers: { 'x-ms-signalr-userid': userId }
};
return config;
}
function newMessage(message: any) {
console.log(message);
}
function newConnection(message: any) {
console.log(message.ConnectionId);
}
And you can find more information Github thread and Js client
With Azure SignalR Service, that mapping is taken care of by the service itself. You would just need to use the userId
itself (which could map to multiple connections, one per connected client) and/or groupName
as applicable.
The userId
itself can be any string like a username or email ID. And the same goes for 'groupName'.
As an example, to work with groups, you would essentially need 3 functions like this
// Will be called by users themselves to connect to Azure SignalR directly.
//
// Example assumes Easy Auth is used
[FunctionName("Negotiate")]
public static SignalRConnectionInfo Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "negotiate")] HttpRequest req,
[SignalRConnectionInfo(HubName = "chat", UserId = "{headers.x-ms-client-principal-id}")]SignalRConnectionInfo connectionInfo,
ILogger log)
{
return connectionInfo;
}
// Used by a group admin or the user themselves to join a group
//
// Example assumes both groupName and userId are passed
[FunctionName("AddToGroup")]
public static Task AddToGroup(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "{groupName}/add/{userId}")]HttpRequest req,
string groupName,
string userId,
[SignalR(HubName = "chat")]IAsyncCollector<SignalRGroupAction> signalRGroupActions)
{
return signalRGroupActions.AddAsync(
new SignalRGroupAction
{
UserId = userId,
GroupName = groupName,
Action = GroupAction.Add
});
}
// Used to send messages to a group. This would involve all users/devices
// added to the group using the previous function
//
// Example assumes both groupName and userId are passed
[FunctionName("SendMessageToGroup")]
public static Task SendMessage(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "{groupName}/send")]object message,
string groupName,
[SignalR(HubName = "chat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
return signalRMessages.AddAsync(
new SignalRMessage
{
// the message will be sent to the group with this name
GroupName = groupName,
Target = "newMessage",
Arguments = new[] { message }
});
}
You can read more about the Azure SignalR Service Bindings for Azure Functions in the official docs.
If you would still want to keep track of connections, you can subscribe to events that the service publishes which return the connectionId
along with the userId
of the client who connected/disconnected from the service.
The SignalRMessage
and SignalRGroupAction
classes support both connectionId
and userId
to identify connections and users respectively.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With