QR Code Pings With Azure Functions and Azure SignalR

A fun little project where a QR code gets an animated rainbow border when it's scanned. The post contains a working demo and an explanation of all the steps behind it using Azure Functions and an Azure SignalR Service.

QR Code Pings With Azure Functions and Azure SignalR
Background by Hannath hanhyder / Vecteezy

I saw this post on Reddit: "I have this qr code sitting behind me in zoom calls. If someone scans it, the light comes on" and I thought "What a fun little idea. I've been meaning to poke around with Azure Functions for a while, this sounds perfect".

Let's begin with the finished product. I present to you a QR code that will have a second of rainbow border when the page the QR code links to is opened. (Might take up to 20 seconds to kick in if the backend hasn't been hit in a while).

The amazing QR code

Or if you aren't near a device that can read QR codes you can head directly to the link and enjoy the retro CSS while triggering the effect on the QR code. Feel free to open the developer console too to watch the pings come in.

I'm going in assuming you're familiar with Visual Studio, C# and Azure. Below is a diagram outlining the main components:

Architecture diagram of this project

Essentially it's a hot potato of sending data from the Azure function to this blog page. We'll go through the whole process in this order:

  1. Azure Functions
  2. Azure SignalR
  3. Combining Azure Functions and Azure SignalR
  4. Client (in this case, the blog page you're on right now)

Azure Functions

I knew I wanted the user to be able to hit the Azure Function via HTTP and thankfully it's the one in the Quickstart: Create your first function in Azure using Visual Studio tutorial. Soon enough I could hit my function locally with my browser and return a string. The tutorial also included publishing to Azure - the right click, publish method. I added a new Storage Account and Application Insights too.

And it looks like this, minus the waiting, and a little sped up:

Nice, now I have a simple text/plain string being returned from Azure.

Azure SignalR

I've worked with SignalR before, I think it's awesome and I'm used to creating my own backend and backplane (with Redis) so I thought Azure SignalR must be much easier. And while it ultimately is, but I had a few hiccups along the way in my serverless understanding.

Service Mode

When creating a new instance of Azure SignalR, you're presented with a choice called Service Mode:

Service mode selector.

This was my first hitch. Serverless sounded like what I wanted but "Default" sounded good too. Reading the SignalR FAQ, I see:

For new applications, only default and serverless mode should be used. The main difference is whether you have application servers that establish server connections to the service (i.e. use AddAzureSignalR() to connect to service). If yes use default mode, otherwise use serverless mode.

At the time I didn't fully comprehend what "application servers" meant. Was it a backend that could handle incoming and outgoing connections? Was it for things like logic apps or web apps to connection to? I dug deeper into the documentation specifically for Service Mode.

With Default mode, they describe it as a proxy between clients and your traditional hub. For my previous use cases, this would be between Javascript clients and my ASP.NET backend (which may then rely on a Redis backplane) for interactive and live websites. At first glance I was thinking, "well, this is just an extra layer of complexity" until reading a bit more then clicked, "I can now get rid of my load balancing and have one Azure SignalR instance specifically for my SignalR traffic and safely introduce some state if needed - because of the built in stickiness to the backend servers". Okay cool, it basically gets me all the normal load balancing goods specifically for SignalR if I need it.

Now to this new Serverless mode. According to the documentation there's no hub, no ASP.NET backend for me, but instead there is a mysterious cloud service that handles connections for me.

A question floated in my head, "But hang on, hubs are where I have some logic and where clients call. Part of the magic of SignalR was how easy clients could talk back to servers".

Correct, and if you need that, go to Default. Whereas the use case for Serverless is for sending messages to specific clients or broadcasting too all or a subset of clients. Or as the Azure SignalR Service REST API Documentation nicely puts it:

"In server-less architecture, clients still have persistent connections to Azure SignalR Service. Since there are no application server to handle traffic, clients are in LISTEN mode, which means they can only receive messages but can't send messages. SignalR Service will disconnect any client who sends messages because it is an invalid operation."

Which leads us nicely onto what was my next question in my head, "How then do messages get to the service to then be sent to the clients?" and that's the HTTP based REST API given to us.

Considering I'm just interested in broadcasting to all clients when someone hits the QR code, serverless it is! For me, this was just making a new instance of the Azure SignalR Service via the Azure portal.

Settings I used to create my Azure SignalR Service

Combining Azure Functions and Azure SignalR

I have by this point:

  1. An Azure Function that returns a string, published in Azure
  2. A storage account for the Azure Function
  3. Application Insights for the Azure Function
  4. An Azure SignalR Service instance

This is where I spent a lot of time fiddling, reading other blogs, reading official documentation and hitting a brick wall. I didn't fully understand how the REST call to the serverless model worked and after enough reading the documentation for SignalR Service output binding for Azure Functions is what I wanted, and it's what we'll look at next.

More Code for the Function

In the function published earlier grab Microsoft.Azure.WebJobs.Extensions.SignalRService from NuGet.

Then add the output binding to the parameters of your function. For me this was [SignalR(HubName = "QRCodeRehash")] IAsyncCollector signalRMessages and at this point my function definition looked like:

[FunctionName("Function1")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    [SignalR(HubName = "QRCodeRehash")] IAsyncCollector<SignalRMessage> signalRMessages,
    ILogger log)

(I always find decorators over input parameters odd to look at, but hey they work great)

Time to add in the code. We'll be invoking the method called pingQR, broadcasted our clients with the payload being a small string of "ping" in an array. This will happen each time a GET or POST hits our endpoint. The whole function, including a little tidy up, looks like:

public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            [SignalR(HubName = "QRCodeRehash")] IAsyncCollector<SignalRMessage> signalRMessages,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            await signalRMessages.AddAsync(
                new SignalRMessage
                {
                    Target = "pingQR",
                    Arguments = new[] { "ping" }
                });

            var responseMessage = "Success";

            return new OkObjectResult(responseMessage);
        }
    }

Next up is some negotiation. In the same class, we'll add the following:

[FunctionName("negotiate")]
public static SignalRConnectionInfo GetOrderNotificationsSignalRInfo(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
    [SignalRConnectionInfo(HubName = "QRCodeRehash")] SignalRConnectionInfo connectionInfo)
{
    return connectionInfo;
}

This endpoint acts as an intermediary between our client and the SignalR Service and will give the client what it needs to connect to the service. The negotiation took me a while to understand, but ended up being a tiny amount of code to implement.

Next up, we hook up the function with the SignalR Service.

Dependencies

Add a Service Dependency to your Azure SignalR Service Instance from the Publish screen. For some reason there are two. One of them didn't work for me, I believe it was the first one, and I did have some issue (see coming up) but worked it out in the end.

Adding a Service Dependency to your Azure SignalR Service

Now, I ran into a snag here. When adding the SignalR Service, my key value for the secret was not correctly formatted. My errors were as follows:

Errors when trying to run locally
Microsoft.Azure.WebJobs.Host: Error indexing method 'Function1'.
Microsoft.Azure.WebJobs.Extensions.SignalRService: 
The SignalR Service connection string must be set either via an 'AzureSignalRConnectionString' app setting, 
via an 'AzureSignalRConnectionString' environment variable, 
or directly in code via SignalROptions.ConnectionString 
or SignalRAttribute.ConnectionStringSetting.

Turns out when I looked in my local.settings.json file for my project, the key wasn't well formed. When setting up the dependency via the publish window I got the key ConnectionStrings__Azure__SignalR__ConnectionString when the code was looking for AzureSignalRConnectionString A quick change and it worked as expected.

And while we're here, we can do a few more edits. Add  "AzureSignalRServiceTransportType": "Transient" too. This will prevent a fallback log message from appearing in the client console.

If you're going to test a SignalR client locally add the following to the same file (change the CORS port to whichever port you're using):

  "Host": {
    "LocalHttpPort": 7071,
    "CORS": "https://localhost:44324",
    "CORSCredentials": true
  }

One more thing to check/change. On the Publish page, click "Manage Azure App Service Settings" and copy over the connection string to local too.

Copying local settings to remote settings for AzureSignalRConnectionString

Publish!

CORS

Now we have to set up CORS. Go to your Azure Function in the Azure Portal and head to the CORS options. Here you need to do:

  1. Click CORS under your function app in the Azure Portal
  2. Add your website to the allowed origins list. Would also be handy to put the https://localhost:xxxx address too if you're going to test locally
  3. Tick "Enable Access-Control-Allow-Credentials"
  4. Save (I missed doing this and it cost me 15 minutes of debugging)
Steps for where and what to do with Azure Functions and CORS

And that's it!

Testing

I created a throwaway MVC website to test, but you can use whatever tech you like. The following script was served via the local throwaway website. It connects to the SignalR Service and outputs a message to the console when a message comes in.  

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7/signalr.min.js"></script>
<script>
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("https://qrcoderehash20210302195635.azurewebsites.net/api")
        .configureLogging(signalR.LogLevel.Debug)
        .build();

    async function start() {
        try {
            await connection.start();
        } catch (err) {
            console.log(err);
        }
    };

    connection.on("pingQR", (user, message) => {
        console.log("Received ping");
    });

    connection.onclose(start);
    start();
</script>

The only thing you'd need to change is the URL parameter. I had difficulty understanding whether I needed the /api or not and it turns out I did need it. If you didn't set up your CORS properly, this is where you'd get your error.

Debug output of the SignalR client

With the little "Received ping" output, congratulations! You've successfully done the whole trip!

Troubleshooting

One helpful bit I had was both the log stream and the logs for my Azure Function. The log stream was great for debugging in real time (for some reason my remote debugger didn't work at the time) and the logs were great to see why my code didn't even run to the point of hitting my log messages.

Major places I had trouble with were:

  • Understanding Serverless vs Default
  • Understanding input and output binding
  • Which CORS settings should be used
  • Remote and Local settings

Outside of the already linked pages, I also used these for help and understanding:

How YOU can learn to build real-time Web Apps that scales, using .NET Core, C#, Azure SignalR Service and JavaScript
[object Object]
SignalR Service (Serverless) - Azure Functions
I am confused about SignalR, specifically when using the Azure SignalR Service and then even more so when thinking about the server-less implementation using Azure Functions. I have a web app and an
SignalR negotiate preflight request fails
I have a scenario where my SignalR server and Client cannot run within the same origin because we would like to share the SignalR server for more than one origin. When i run the server locally and ...

Extra Bits

A couple of smaller bits specifically for this project.

Serving Static Content from my Azure Function (The Win98 Page)

For the QR Code endpoint page I wanted to have some small HTML returned by the function. Something a little more than plain text to make it a little nicer. I remember I had bookmarked 98.css and thought this would be a fun time to use it.

I wrote a little HTML file, Success.html, and added it to my project because I wanted it to go along with the publish to Azure. Online I saw others before me ask about this process and a fair amount of people were against the idea saying this isn't what was intended for functions or at the very least, serve the HTML from a blob endpoint.

But since this is for fun, I ignored those gripes, and both make sure the file properties were set to Content and Copy if newer and modified the .csproj file to add CopyToPublishDirectory tags too. Had I not done this, I would've had to go into Kudu and manually upload Success.html there - and do that every time I wanted to update.

On publish I learned the file will end up in D:\home\site\wwwroot. Firing up Kudu, I did indeed see my file there.

Debug console view of my Azure Function

How do I then serve this file? After a quick search I found what I needed:

string htmlFilePath = Path.Combine(context.FunctionAppDirectory, "Success.html");
string content = File.ReadAllText(htmlFilePath);

And that's it. Not too bad.

QR Code

For the QR code itself, I used a free online generator.

For the rainbow effect I did it quickly by having around a 5px transparent line around the QR code image then using the idea from this Codepen I made the background change which would come through on the 5px around the outside of the image.

This is my final result for what is used on this page. Note: that I'm sending a single argument from the function but expecting two on the client side (user, message) - an error on my part but this worked for the JS client as JS will just assign undefined to the latter input variable, but will not work for the .NET (or potentially other) clients:

<style>
    .wrapper {
        background: linear-gradient(124deg, #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, #1ddde8, #2b1de8, #dd00f3, #dd00f3);
        background-size: 1800% 1800%;

        animation-name: rainbow;
        animation-duration: 2s;
        animation-timing-function: ease;
        animation-iteration-count: 1;
    }

    @keyframes rainbow {
        0% {
            background-position: 0% 82%
        }

        50% {
            background-position: 100% 19%
        }

        100% {
            background-position: 0% 82%
        }
    }

    .bg-white {
        background-color: "#FFF";
    }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7/signalr.min.js"></script>
<script>
    (function () {
        var timeoutHandle = 0;
        var imageElement = document.querySelectorAll(".kg-image")[0];

        document.querySelectorAll(".kg-image")[0].classList.add("bg-white")

        const connection = new signalR.HubConnectionBuilder()
            .withUrl("https://qrcodetriggerfunction.azurewebsites.net/api")
            .configureLogging(signalR.LogLevel.Debug)
            .build();

        async function start() {
            try {
                await connection.start();
                setTimeout(() => connection.stop(), 1000 * 60 * 60);
            } catch (err) {
                console.log(err);
                //setTimeout(start, 5000);
            }
        };

        const startAnimation = () => {
            imageElement.classList.add("wrapper")
            timeoutHandle = setTimeout(() => imageElement.classList.remove("wrapper"), 1000);
        }

        connection.on("pingQR", (user, message) => {
            console.log("Received ping");
            if (timeoutHandle !== 0) {
                clearTimeout(timeoutHandle);
                timeoutHandle = 0;
                imageElement.style.webkitAnimation = 'none';
                setTimeout(function () {
                    imageElement.style.webkitAnimation = '';
                    imageElement.classList.remove("wrapper")
                    startAnimation();
                }, 10);          
            } else {
                startAnimation();
            }

         
        });

        connection.onclose(start);
        start();
    })();
</script>

I wanted the animation to restart if the animation was mid-way when a new ping came through. Turns out it's not as easy and I followed this StackOverflow answer to get what was good enough for the effort I wanted to put in.

To Conclude

I hope you enjoyed this long post. I had a great time learning these new bits. Keep in mind that Azure moves fast and some of this may be out of date by the time you're reading this, but I'm sure you'll work it out!