| title | Tutorial - Create a serverless notification app using Azure Web PubSub service and Azure Functions |
|---|---|
| description | A tutorial to walk through how to use Azure Web PubSub service and Azure Functions to build a serverless notification application. |
| author | JialinXin |
| ms.author | jixin |
| ms.service | azure-web-pubsub |
| ms.custom | devx-track-azurecli |
| ms.topic | tutorial |
| ms.date | 01/12/2024 |
The Azure Web PubSub service helps you build real-time messaging web applications using WebSockets. Azure Functions is a serverless platform that lets you run your code without managing any infrastructure. In this tutorial, you learn how to use Azure Web PubSub service and Azure Functions to build a serverless application with real-time messaging under notification scenarios.
In this tutorial, you learn how to:
[!div class="checklist"]
- Build a serverless notification app
- Work with Web PubSub function input and output bindings
- Run the sample functions locally
- Deploy the function to Azure Function App
[!INCLUDE Connection string security]
-
A code editor, such as Visual Studio Code
-
Node.js, version 18.x or above.
[!NOTE] For more information about the supported versions of Node.js, see Azure Functions runtime versions documentation.
-
Azure Functions Core Tools (V4 or higher preferred) to run Azure Function apps locally and deploy to Azure.
-
The Azure CLI to manage Azure resources.
-
A code editor, such as Visual Studio Code
-
Node.js, version 18.x or above.
[!NOTE] For more information about the supported versions of Node.js, see Azure Functions runtime versions documentation.
-
Azure Functions Core Tools (V4 or higher preferred) to run Azure Function apps locally and deploy to Azure.
-
The Azure CLI to manage Azure resources.
-
A code editor, such as Visual Studio Code.
-
Azure Functions Core Tools (v4 or higher preferred) to run Azure Function apps locally and deploy to Azure.
-
The Azure CLI to manage Azure resources.
-
A code editor, such as Visual Studio Code.
-
Azure Functions Core Tools (v4 or higher preferred) to run Azure Function apps locally and deploy to Azure.
-
The Azure CLI to manage Azure resources.
-
A code editor, such as Visual Studio Code.
-
Python (v3.7+). See supported Python versions.
-
Azure Functions Core Tools (V4 or higher preferred) to run Azure Function apps locally and deploy to Azure.
-
The Azure CLI to manage Azure resources.
[!INCLUDE quickstarts-free-trial-note]
[!INCLUDE create-instance-portal]
-
Make sure you have Azure Functions Core Tools installed. Now, create an empty directory for the project. Run command under this working directory. Use one of the given options below.
func init --worker-runtime javascript --model V4
func init --worker-runtime javascript --model V3
func init --worker-runtime dotnet
func init --worker-runtime dotnet-isolated
func init --worker-runtime python --model V1
-
Follow the steps to install
Microsoft.Azure.WebJobs.Extensions.WebPubSub.Confirm or update
host.json's extensionBundle to version 4.* or later to get Web PubSub support. For updating thehost.json, open the file in editor, and then replace the existing version extensionBundle to version 4.* or later.{ "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[4.*, 5.0.0)" } }Confirm or update
host.json's extensionBundle to version 3.3.0 or later to get Web PubSub support. For updating thehost.json, open the file in editor, and then replace the existing version extensionBundle to version 3.3.0 or later.{ "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[3.3.*, 4.0.0)" } }dotnet add package Microsoft.Azure.WebJobs.Extensions.WebPubSub
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.WebPubSub --prerelease
Update
host.json's extensionBundle to version 3.3.0 or later to get Web PubSub support. For updating thehost.json, open the file in editor, and then replace the existing version extensionBundle to version 3.3.0 or later.{ "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[3.3.*, 4.0.0)" } } -
Create an
indexfunction to read and host a static web page for clients.func new -n index -t HttpTrigger
- Update
src/functions/index.jsand copy following codes.const { app } = require('@azure/functions'); const { readFile } = require('fs/promises'); app.http('index', { methods: ['GET', 'POST'], authLevel: 'anonymous', handler: async (context) => { const content = await readFile('index.html', 'utf8', (err, data) => { if (err) { context.err(err) return } }); return { status: 200, headers: { 'Content-Type': 'text/html' }, body: content, }; } });
- Update
index/function.jsonand copy following json codes.{ "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "methods": [ "get", "post" ] }, { "type": "http", "direction": "out", "name": "res" } ] } - Update
index/index.jsand copy following codes.var fs = require('fs'); var path = require('path'); module.exports = function (context, req) { var index = context.executionContext.functionDirectory + '/../index.html'; context.log("index.html path: " + index); fs.readFile(index, 'utf8', function (err, data) { if (err) { console.log(err); context.done(err); } context.res = { status: 200, headers: { 'Content-Type': 'text/html' }, body: data }; context.done(); }); }
- Update
index.csand replaceRunfunction with following codes.[FunctionName("index")] public static IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req, ExecutionContext context, ILogger log) { var indexFile = Path.Combine(context.FunctionAppDirectory, "index.html"); log.LogInformation($"index.html path: {indexFile}."); return new ContentResult { Content = File.ReadAllText(indexFile), ContentType = "text/html", }; }
- Update
index.csand replaceRunfunction with following codes.[Function("index")] public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, FunctionContext context) { var path = Path.Combine(context.FunctionDefinition.PathToAssembly, "../index.html"); _logger.LogInformation($"index.html path: {path}."); var response = req.CreateResponse(); response.WriteString(File.ReadAllText(path)); response.Headers.Add("Content-Type", "text/html"); return response; }
- Update
index/function.jsonand copy following json codes.{ "scriptFile": "__init__.py", "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "methods": [ "get", "post" ] }, { "type": "http", "direction": "out", "name": "$return" } ] } - Update
index/__init__.pyand copy following codes.import os import azure.functions as func def main(req: func.HttpRequest) -> func.HttpResponse: f = open(os.path.dirname(os.path.realpath(__file__)) + '/../index.html') return func.HttpResponse(f.read(), mimetype='text/html')
- Update
-
Create a
negotiatefunction to help clients get service connection url with access token.func new -n negotiate -t HttpTrigger
- Update
src/functions/negotiate.jsand copy following codes.const { app, input } = require('@azure/functions'); const connection = input.generic({ type: 'webPubSubConnection', name: 'connection', hub: 'notification' }); app.http('negotiate', { methods: ['GET', 'POST'], authLevel: 'anonymous', extraInputs: [connection], handler: async (request, context) => { return { body: JSON.stringify(context.extraInputs.get('connection')) }; }, });
- Update
negotiate/function.jsonand copy following json codes.{ "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req" }, { "type": "http", "direction": "out", "name": "res" }, { "type": "webPubSubConnection", "name": "connection", "hub": "notification", "direction": "in" } ] } - Create a folder negotiate and update
negotiate/index.jsand copy following codes.module.exports = function (context, req, connection) { context.res = { body: connection }; context.done(); };
- Update
negotiate.csand replaceRunfunction with following codes.[FunctionName("negotiate")] public static WebPubSubConnection Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, [WebPubSubConnection(Hub = "notification")] WebPubSubConnection connection, ILogger log) { log.LogInformation("Connecting..."); return connection; }
- Add
usingstatements in header to resolve required dependencies.using Microsoft.Azure.WebJobs.Extensions.WebPubSub;
- Update
negotiate.csand replaceRunfunction with following codes.[Function("negotiate")] public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, [WebPubSubConnectionInput(Hub = "notification")] WebPubSubConnection connectionInfo) { var response = req.CreateResponse(HttpStatusCode.OK); response.WriteAsJsonAsync(connectionInfo); return response; }
- Create a folder negotiate and update
negotiate/function.jsonand copy following json codes.{ "scriptFile": "__init__.py", "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req" }, { "type": "http", "direction": "out", "name": "$return" }, { "type": "webPubSubConnection", "name": "connection", "hub": "notification", "direction": "in" } ] } - Update
negotiate/__init__.pyand copy following codes.import logging import azure.functions as func def main(req: func.HttpRequest, connection) -> func.HttpResponse: return func.HttpResponse(connection)
- Update
-
Create a
notificationfunction to generate notifications withTimerTrigger.func new -n notification -t TimerTrigger
- Update
src/functions/notification.jsand copy following codes.const { app, output } = require('@azure/functions'); const wpsAction = output.generic({ type: 'webPubSub', name: 'action', hub: 'notification' }); app.timer('notification', { schedule: "*/10 * * * * *", extraOutputs: [wpsAction], handler: (myTimer, context) => { context.extraOutputs.set(wpsAction, { actionName: 'sendToAll', data: `[DateTime: ${new Date()}] Temperature: ${getValue(22, 1)}\xB0C, Humidity: ${getValue(40, 2)}%`, dataType: 'text', }); }, }); function getValue(baseNum, floatNum) { return (baseNum + 2 * floatNum * (Math.random() - 0.5)).toFixed(3); }
- Update
notification/function.jsonand copy following json codes.{ "bindings": [ { "name": "myTimer", "type": "timerTrigger", "direction": "in", "schedule": "*/10 * * * * *" }, { "type": "webPubSub", "name": "actions", "hub": "notification", "direction": "out" } ] } - Update
notification/index.jsand copy following codes.module.exports = function (context, myTimer) { context.bindings.actions = { "actionName": "sendToAll", "data": `[DateTime: ${new Date()}] Temperature: ${getValue(22, 1)}\xB0C, Humidity: ${getValue(40, 2)}%`, "dataType": "text" } context.done(); }; function getValue(baseNum, floatNum) { return (baseNum + 2 * floatNum * (Math.random() - 0.5)).toFixed(3); }
- Update
notification.csand replaceRunfunction with following codes.[FunctionName("notification")] public static async Task Run([TimerTrigger("*/10 * * * * *")]TimerInfo myTimer, ILogger log, [WebPubSub(Hub = "notification")] IAsyncCollector<WebPubSubAction> actions) { await actions.AddAsync(new SendToAllAction { Data = BinaryData.FromString($"[DateTime: {DateTime.Now}] Temperature: {GetValue(23, 1)}{'\xB0'}C, Humidity: {GetValue(40, 2)}%"), DataType = WebPubSubDataType.Text }); } private static string GetValue(double baseNum, double floatNum) { var rng = new Random(); var value = baseNum + floatNum * 2 * (rng.NextDouble() - 0.5); return value.ToString("0.000"); }
- Add
usingstatements in header to resolve required dependencies.using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Extensions.WebPubSub; using Microsoft.Azure.WebPubSub.Common;
- Update
notification.csand replaceRunfunction with following codes.[Function("notification")] [WebPubSubOutput(Hub = "notification")] public SendToAllAction Run([TimerTrigger("*/10 * * * * *")] MyInfo myTimer) { return new SendToAllAction { Data = BinaryData.FromString($"[DateTime: {DateTime.Now}] Temperature: {GetValue(23, 1)}{'\xB0'}C, Humidity: {GetValue(40, 2)}%"), DataType = WebPubSubDataType.Text }; } private static string GetValue(double baseNum, double floatNum) { var rng = new Random(); var value = baseNum + floatNum * 2 * (rng.NextDouble() - 0.5); return value.ToString("0.000"); }
- Create a folder notification and update
notification/function.jsonand copy following json codes.{ "scriptFile": "__init__.py", "bindings": [ { "name": "myTimer", "type": "timerTrigger", "direction": "in", "schedule": "*/10 * * * * *" }, { "type": "webPubSub", "name": "actions", "hub": "notification", "direction": "out" } ] } - Update
notification/__init__.pyand copy following codes.import datetime import random import json import azure.functions as func def main(myTimer: func.TimerRequest, actions: func.Out[str]) -> None: time = datetime.datetime.now().strftime("%A %d-%b-%Y %H:%M:%S") actions.set(json.dumps({ 'actionName': 'sendToAll', 'data': '\x5B DateTime: {0} \x5D Temperature: {1:.3f} \xB0C, Humidity: {2:.3f} \x25'.format(time, 22 + 2 * (random.random() - 0.5), 44 + 4 * (random.random() - 0.5)), 'dataType': 'text' }))
- Update
-
Add the client single page
index.htmlin the project root folder and copy content.<html> <body> <h1>Azure Web PubSub Notification</h1> <div id="messages"></div> <script> (async function () { let messages = document.querySelector('#messages'); let res = await fetch(`${window.location.origin}/api/negotiate`); let url = await res.json(); let ws = new WebSocket(url.url); ws.onopen = () => console.log('connected'); ws.onmessage = event => { let m = document.createElement('p'); m.innerText = event.data; messages.appendChild(m); }; })(); </script> </body> </html>
Since C# project compiles files to a different output folder, you need to update your
*.csprojto make the content page go with it.<ItemGroup> <None Update="index.html"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>
Since C# project compiles files to a different output folder, you need to update your
*.csprojto make the content page go with it.<ItemGroup> <None Update="index.html"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>
-
Configure and run the Azure Function app
[!INCLUDE Connection string security comment]
- In the browser, open the Azure portal and confirm the Web PubSub Service instance you deployed earlier was successfully created. Navigate to the instance.
- Select Keys and copy out the connection string.
:::image type="content" source="media/quickstart-serverless/copy-connection-string.png" alt-text="Screenshot of copying the Web PubSub connection string.":::
Run command in the function folder to set the service connection string. Replace
<connection-string>with your value as needed.func settings add WebPubSubConnectionString "<connection-string>"[!NOTE]
TimerTriggerused in the sample has dependency on Azure Storage, but you can use local storage emulator when the Function is running locally. If you got some error likeThere was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid., you'll need to download and enable Storage Emulator.Now you're able to run your local function by command.
func start --port 7071
And checking the running logs, you can visit your local host static page by visiting:
http://localhost:7071/api/index.[!NOTE] Some browsers automatically redirect to
httpsthat leads to wrong url. Suggest to useEdgeand double check the url if rendering is not success.
Before you can deploy your function code to Azure, you need to create three resources:
- A resource group, which is a logical container for related resources.
- A storage account, which is used to maintain state and other information about your functions.
- A function app, which provides the environment for executing your function code. A function app maps to your local function project and lets you group functions as a logical unit for easier management, deployment and sharing of resources.
Use the following commands to create these items.
-
Sign in to Azure:
az login -
Create a resource group or you can skip by reusing the one of Azure Web PubSub service:
az group create -n WebPubSubFunction -l <REGION> -
Create a general-purpose storage account in your resource group and region:
az storage account create -n <STORAGE_NAME> -l <REGION> -g WebPubSubFunction -
Create the function app in Azure:
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime node --runtime-version 18 --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>[!NOTE] Check Azure Functions runtime versions documentation to set
--runtime-versionparameter to supported value.az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime node --runtime-version 18 --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>[!NOTE] Check Azure Functions runtime versions documentation to set
--runtime-versionparameter to supported value.az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime dotnet --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime dotnet-isolated --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime python --runtime-version 3.9 --functions-version 4 --name <FUNCIONAPP_NAME> --os-type linux --storage-account <STORAGE_NAME> -
Deploy the function project to Azure:
Once you create your function app in Azure, you're now ready to deploy your local functions project by using the func azure functionapp publish command.
func azure functionapp publish <FUNCIONAPP_NAME> --publish-local-settings
[!NOTE] Here we are deploying local settings
local.settings.jsontogether with command parameter--publish-local-settings. If you're using Microsoft Azure Storage Emulator, you can typenoto skip overwriting this value on Azure following the prompt message:App setting AzureWebJobsStorage is different between azure and local.settings.json, Would you like to overwrite value in azure? [yes/no/show]. Besides, you can update Function App settings in Azure portal -> Settings -> Configuration. -
Now you can check your site from Azure Function App by navigating to URL:
https://<FUNCIONAPP_NAME>.azurewebsites.net/api/index.
If you're not going to continue to use this app, delete all resources created by this doc with the following steps so you don't incur any charges:
-
In the Azure portal, select Resource groups on the far left, and then select the resource group you created. Use the search box to find the resource group by its name instead.
-
In the window that opens, select the resource group, and then select Delete resource group.
-
In the new window, type the name of the resource group to delete, and then select Delete.
In this quickstart, you learned how to run a serverless chat application. Now, you could start to build your own application.
[!div class="nextstepaction"] Tutorial: Create a simple chatroom with Azure Web PubSub
[!div class="nextstepaction"] Azure Web PubSub bindings for Azure Functions
[!div class="nextstepaction"] Explore more Azure Web PubSub samples