12

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.

2 Answers 2

6

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.

Sign up to request clarification or add additional context in comments.

8 Comments

do you have an example of how to call the AddToGroup http function? I'm at a loss how to get the userId that is in your route. I can't find it on the SignalR JavaScript client connection object
by doing a post to: {functionUrl}/{groupName}/add/{userId} @JasonCoder
As @pmeyer mentioned, you would have to make a POST call with the groupName and userId. In this example, the userId is the principal id sent in the header when EasyAuth is enabled on the function app. For AddToGroup, either the group admin would have to make the call (knowing the principal id through some other reference) or the users would add themselves, in which case you would have to get the principal id from the header instead of the route.
@PramodValavala-MSFT how are you sending the message object in the SendMessageToGroup method, don't you have to read it from the body manually?
Not really helpful. With signalR hosted on my asp.net/aspnet core app I can call actions of the hub from the hub client. On those methods plus the OnConnect/OnDisconnect the hub would among other things manage the connection's group membership. A POST to a random url is not helpful unless it shares some piece of information with the connection. If while connecting on my hub I have for example a query string argument that identifies the user somehow then and that POST on the azure function has the same identifying information then it makes sense. The above does not show this at all.
|
2

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

1 Comment

Thank you for your response, but I am using Azure's EasyAuth, so.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.