Muokkaa

Jaa


Azure Functions development and configuration with Azure SignalR Service

Azure Functions applications can use the Azure SignalR Service bindings to add real-time capabilities. Client applications use client SDKs available in several languages to connect to Azure SignalR Service and receive real-time messages.

This article describes the concepts for developing and configuring an Azure Function app that is integrated with SignalR Service.

Important

Raw connection strings appear in this article for demonstration purposes only.

A connection string includes the authorization information required for your application to access Azure Web PubSub service. The access key inside the connection string is similar to a root password for your service. In production environments, always protect your access keys. Use Azure Key Vault to manage and rotate your keys securely and secure your connection string using Microsoft Entra ID and authorize access with Microsoft Entra ID.

Avoid distributing access keys to other users, hard-coding them, or saving them anywhere in plain text that is accessible to others. Rotate your keys if you believe they may have been compromised.

SignalR Service configuration

Azure SignalR Service can be configured in different modes. When used with Azure Functions, the service must be configured in Serverless mode.

In the Azure portal, locate the Settings page of your SignalR Service resource. Set the Service mode to Serverless.

SignalR Service Mode

Azure Functions development

A serverless real-time application built with Azure Functions and Azure SignalR Service requires at least two Azure Functions:

  • A negotiate function that the client calls to obtain a valid SignalR Service access token and endpoint URL.
  • One or more functions that handle messages sent from SignalR Service to clients.

Negotiation function

A client application requires a valid access token to connect to Azure SignalR Service. An access token can be anonymous or authenticated to a user ID. Serverless SignalR Service applications require an HTTP endpoint named negotiate to obtain a token and other connection information, such as the SignalR Service endpoint URL.

Use an HTTP-triggered Azure Function and the SignalRConnectionInfo input binding to generate the connection information object. The function must have an HTTP route that ends in /negotiate.

With class-based model in C#, you don't need the SignalRConnectionInfo input binding and can add custom claims much more easily. For more information, see Negotiation experience in class-based model.

For more information about the negotiate function, see Azure Functions development.

To learn how to create an authenticated token, refer to Using App Service Authentication.

Handle messages sent from SignalR Service

Use the SignalRTrigger binding to handle messages sent from SignalR Service. You can get notified when clients send messages or clients get connected or disconnected.

For more information, see the SignalR Service trigger binding reference.

You also need to configure your function endpoint as an upstream endpoint so that service triggers the function when there's message from a client. For more information about how to configure upstream endpoints, see Upstream endpoints.

Note

SignalR Service doesn't support the StreamInvocation message from a client in Serverless Mode.

Sending messages and managing group membership

Use the SignalR output binding to send messages to clients connected to Azure SignalR Service. You can broadcast messages to all clients, or you can send them to a subset of clients. For example, only send messages to clients authenticated with a specific user ID, or only to a specific group.

Users can be added to one or more groups. You can also use the SignalR output binding to add or remove users to/from groups.

For more information, see the SignalR output binding reference.

SignalR Hubs

SignalR has a concept of hubs. Each client connection and each message sent from Azure Functions is scoped to a specific hub. You can use hubs as a way to separate your connections and messages into logical namespaces.

Class-based model

The class-based model is dedicated for C#.

The class-based model provides better programming experience, which can replace SignalR input and output bindings, with the following features:

  • More flexible negotiation, sending messages and managing groups experience.
  • More managing functionalities are supported, including closing connections, checking whether a connection, user, or group exists.
  • Strongly typed hub
  • Unified hub name and connection string setting in one place.

The following code demonstrates how to write SignalR bindings in class-based model:

Firstly, define your hub derived from a class ServerlessHub:

[SignalRConnection("AzureSignalRConnectionString")]
public class Functions : ServerlessHub
{
    private const string HubName = nameof(Functions); // Used by SignalR trigger only

    public Functions(IServiceProvider serviceProvider) : base(serviceProvider)
    {
    }

    [Function("negotiate")]
    public async Task<HttpResponseData> Negotiate([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
    {
        var negotiateResponse = await NegotiateAsync(new() { UserId = req.Headers.GetValues("userId").FirstOrDefault() });
        var response = req.CreateResponse();
        response.WriteBytes(negotiateResponse.ToArray());
        return response;
    }

    [Function("Broadcast")]
    public Task Broadcast(
    [SignalRTrigger(HubName, "messages", "broadcast", "message")] SignalRInvocationContext invocationContext, string message)
    {
        return Clients.All.SendAsync("newMessage", new NewMessage(invocationContext, message));
    }

    [Function("JoinGroup")]
    public Task JoinGroup([SignalRTrigger(HubName, "messages", "JoinGroup", "connectionId", "groupName")] SignalRInvocationContext invocationContext, string connectionId, string groupName)
    {
        return Groups.AddToGroupAsync(connectionId, groupName);
    }
}

In the Program.cs file, register your serverless hub:

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(b => b.Services
        .AddServerlessHub<Functions>())
    .Build();

Negotiation experience in class-based model

Instead of using SignalR input binding [SignalRConnectionInfoInput], negotiation in class-based model can be more flexible. Base class ServerlessHub has a method NegotiateAsync, which allows users to customize negotiation options such as userId, claims, etc.

Task<BinaryData> NegotiateAsync(NegotiationOptions? options = null)

Sending messages and managing experience in class-based model

You could send messages, manage groups, or manage clients by accessing the members provided by base class ServerlessHub.

  • ServerlessHub.Clients for sending messages to clients.
  • ServerlessHub.Groups for managing connections with groups, such as adding connections to groups, removing connections from groups.
  • ServerlessHub.UserGroups for managing users with groups, such as adding users to groups, removing users from groups.
  • ServerlessHub.ClientManager for checking connections existence, closing connections, etc.

Strongly typed Hub

Strongly typed hub allows you to use strongly typed methods when you send messages to clients. To use strongly typed hub in class based model, extract client methods into an interface T, and make your hub class derived from ServerlessHub<T>.

The following code is an interface sample for client methods.

public interface IChatClient
{
    Task newMessage(NewMessage message);
}

Then you can use the strongly typed methods as follows.

Raw connection strings appear in this article for demonstration purposes only. In production environments, always protect your access keys. Use Azure Key Vault to manage and rotate your keys securely and secure your connection string using Microsoft Entra ID and authorize access with Microsoft Entra ID.

[SignalRConnection("AzureSignalRConnectionString")]
public class Functions : ServerlessHub<IChatClient>
{
    private const string HubName = nameof(Functions);  // Used by SignalR trigger only

    public Functions(IServiceProvider serviceProvider) : base(serviceProvider)
    {
    }

    [Function("Broadcast")]
    public Task Broadcast(
    [SignalRTrigger(HubName, "messages", "broadcast", "message")] SignalRInvocationContext invocationContext, string message)
    {
        return Clients.All.newMessage(new NewMessage(invocationContext, message));
    }
}

Note

You can get a complete project sample from GitHub.

Unified hub name and connection string setting in one place

  • The class name of the serverless hub is automatically used as HubName.
  • You might have noticed the SignalRConnection attribute used on serverless hub classes as follows:
    [SignalRConnection("AzureSignalRConnectionString")]
    public class Functions : ServerlessHub<IChatClient>
    
    It allows you to customize where the connection string for serverless hub is. If it's absent, the default value AzureSignalRConnectionString is used.

Important

SignalR triggers and serverless hubs are independent. Therefore, the class name of serverless hub and SignalRConnection attribute doesn't change the settings of SignalR triggers, even though you use SignalR triggers inside the serverless hub.

Client development

SignalR client applications can use the SignalR client SDK in one of several languages to easily connect to and receive messages from Azure SignalR Service.

Configuring a client connection

To connect to SignalR Service, a client must complete a successful connection negotiation that consists of these steps:

  1. Make a request to the negotiate HTTP endpoint discussed above to obtain valid connection information
  2. Connect to SignalR Service using the service endpoint URL and access token obtained from the negotiate endpoint

SignalR client SDKs already contain the logic required to perform the negotiation handshake. Pass the negotiation endpoint's URL, minus the negotiate segment, to the SDK's HubConnectionBuilder. Here's an example in JavaScript:

const connection = new signalR.HubConnectionBuilder()
  .withUrl("https://my-signalr-function-app.azurewebsites.net/api")
  .build();

By convention, the SDK automatically appends /negotiate to the URL and uses it to begin the negotiation.

Note

If you are using the JavaScript/TypeScript SDK in a browser, you need to enable cross-origin resource sharing (CORS) on your Function App.

For more information on how to use the SignalR client SDK, see the documentation for your language:

Sending messages from a client to the service

If you've upstream configured for your SignalR resource, you can send messages from a client to your Azure Functions using any SignalR client. Here's an example in JavaScript:

connection.send("method1", "arg1", "arg2");

Azure Functions configuration

Azure Function apps that integrate with Azure SignalR Service can be deployed like any typical Azure Function app, using techniques such as continuously deployment, zip deployment, and run from package.

However, there are a couple of special considerations for apps that use the SignalR Service bindings. If the client runs in a browser, CORS must be enabled. And if the app requires authentication, you can integrate the negotiation endpoint with App Service Authentication.

Enabling CORS

The JavaScript/TypeScript client makes HTTP request to the negotiation function to initiate the connection negotiation. When the client application is hosted on a different domain than the Azure Function app, cross-origin resource sharing (CORS) must be enabled on the function app, or the browser will block the requests.

Localhost

When running the Function app on your local computer, you can add a Host section to local.settings.json to enable CORS. In the Host section, add two properties:

  • CORS - enter the base URL that is the origin the client application
  • CORSCredentials - set it to true to allow "withCredentials" requests

Example:

{
  "IsEncrypted": false,
  "Values": {
    // values
  },
  "Host": {
    "CORS": "http://localhost:8080",
    "CORSCredentials": true
  }
}

Cloud - Azure Functions CORS

To enable CORS on an Azure Function app, go to the CORS configuration screen under the Platform features tab of your Function app in the Azure portal.

Note

CORS configuration is not yet available in Azure Functions Linux Consumption plan. Use Azure API Management to enable CORS.

CORS with Access-Control-Allow-Credentials must be enabled for the SignalR client to call the negotiation function. To enable it, select the checkbox.

In the Allowed origins section, add an entry with the origin base URL of your web application.

Configuring CORS

Cloud - Azure API Management

Azure API Management provides an API gateway that adds capabilities to existing back-end services. You can use it to add CORS to your function app. It offers a consumption tier with pay-per-action pricing and a monthly free grant.

Refer to the API Management documentation for information on how to import an Azure Function app. Once imported, you can add an inbound policy to enable CORS with Access-Control-Allow-Credentials support.

<cors allow-credentials="true">
  <allowed-origins>
    <origin>https://azure-samples.github.io</origin>
  </allowed-origins>
  <allowed-methods>
    <method>GET</method>
    <method>POST</method>
  </allowed-methods>
  <allowed-headers>
    <header>*</header>
  </allowed-headers>
  <expose-headers>
    <header>*</header>
  </expose-headers>
</cors>

Configure your SignalR clients to use the API Management URL.

Using App Service Authentication

Azure Functions has built-in authentication, supporting popular providers such as Facebook, X, Microsoft Account, Google, and Microsoft Entra ID. This feature can be integrated with the SignalRConnectionInfo binding to create connections to Azure SignalR Service that is authenticated to a user ID. Your application can send messages using the SignalR output binding that are targeted to that user ID.

In the Azure portal, in your Function app's Platform features tab, open the Authentication/authorization settings window. Follow the documentation for App Service Authentication to configure authentication using an identity provider of your choice.

Once configured, authenticated HTTP requests include x-ms-client-principal-name and x-ms-client-principal-id headers containing the authenticated identity's username and user ID, respectively.

You can use these headers in your SignalRConnectionInfo binding configuration to create authenticated connections. Here's an example C# negotiation function that uses the x-ms-client-principal-id header.

[FunctionName("negotiate")]
public static SignalRConnectionInfo Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req,
    [SignalRConnectionInfo
        (HubName = "chat", UserId = "{headers.x-ms-client-principal-id}")]
        SignalRConnectionInfo connectionInfo)
{
    // connectionInfo contains an access key token with a name identifier claim set to the authenticated user
    return connectionInfo;
}

You can then send messages to that user by setting the UserId property of a SignalR message.

[FunctionName("SendMessage")]
public static Task SendMessage(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")]object message,
    [SignalR(HubName = "chat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
    return signalRMessages.AddAsync(
        new SignalRMessage
        {
            // the message will only be sent to these user IDs
            UserId = "userId1",
            Target = "newMessage",
            Arguments = new [] { message }
        });
}

For information on other languages, see the Azure SignalR Service bindings for Azure Functions reference.

Next steps

In this article, you learn how to develop and configure serverless SignalR Service applications using Azure Functions. Try creating an application yourself using one of the quick starts or tutorials on the SignalR Service overview page.