DEV Community

Cover image for How YOU can learn to build real-time Web Apps that scales, using .NET Core, C#, Azure SignalR Service and JavaScript
Chris Noring for Microsoft Azure

Posted on • Updated on • Originally published at softchris.github.io

How YOU can learn to build real-time Web Apps that scales, using .NET Core, C#, Azure SignalR Service and JavaScript

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

Ok, so you want to build real-time applications? There are some things to consider like what do I do if the browser doesn't support Web Sockets, what's my fallback technology? Also, how do I scale? What about CORS? As you can see there's more to it than just creating a Web Socket. That's why SignalR exists to tackle the above scenarios.

TLDR; There are two things this article will tackle, one is SignalR itself, what it is and why use it. We will also go into Azure SignalR service and talk about the difference. Lastly, we will show a demo using SignalR service and Serverless.

This article is part of #25DaysOfServerless. New challenges will be published every day from Microsoft Cloud Advocates throughout the month of December. Find out more about how Microsoft Azure enables your Serverless functions.

References

  • Sign up for a free Azure account
    To be able to use the Azure SignalR Service part you will need a free Azure account

  • SignalR overview
    A great page that explains what SignalR is, how it works etc.

  • ASP.NET Core SignalR
    Great overview. Not as detail-heavy as the first page but still covers the basic concepts well a TLDR; version if you will.

  • SignalR GitHub repo
    It's open-source and contains examples using different languages for the Serverless part and also clients with and without Auth.

  • SginalR + .NET Core Tutorial
    This tutorial covers how to build a SignalR backend in .NET Core Web project and how we call it from a Client.

SignalR

ASP.NET SignalR is a library for ASP.NET developers that simplifies the process of adding real-time web functionality to applications. Real-time web functionality is the ability to have server code push content to connected clients instantly as it becomes available, rather than having the server wait for a client to request new data.

What can I use it for?

While chat is often used as an example, you can do a whole lot more like Dashboards and monitoring applications, collaborative applications (such as simultaneous editing of documents), job progress updates, and real-time forms.

How do I recognize when I should be using SignalR?

Any time a user refreshes a web page to see new data, or the page implements long polling to retrieve new data, it is a candidate for using SignalR.

How does it work?

SignalR provides a simple API for creating server-to-client remote procedure calls (RPC) that call JavaScript functions in client browsers (and other client platforms) from server-side .NET code.

Ok, so SignalR calls my JavaScript code when there's new data?

Correct.

SignalR handles connection management automatically and lets you broadcast messages to all connected clients simultaneously, like a chat room. You can also send messages to specific clients.

Broadcast and I can also target specific clients, got it.

SignalR uses the new WebSocket transport where available and falls back to older transports where necessary. While you could certainly write your app using WebSocket directly, using SignalR means that a lot of the extra functionality you would need to implement is already done for you.

Oh, so I could be using WebSockets instead of SignalR but if that doesn't work I would need to code a fallback myself. But if I use SignalR, I don't need to care? I get WebSocket primarily but a fallback behavior?

Correct.

Hosting

There are two ways to host SignalR:

  • Self-hosted, we host SignalR ourselves as part of a Web App
  • Azure SignalR Service, this is SignalR living in the Cloud as a service, it comes with a lot of benefits

Here's an overview:

 Azure SignalR Service

Why should I go with the Service over self-hosted?

Switching to SignalR Service will remove the need to manage backplanes that handle the scales and client connections.

Ok so you handle client connections for me and scaling. Nice, I like the sound of that.

The fully managed service also simplifies web applications and saves hosting costs.

Simplifications and saves me money. No objections from me :)

SignalR Service offers global reach and world-class data center and network, scales to millions of connections, guarantees SLA, while providing all the compliance and security at Azure standard.

Millions of connections! That's a large chat room ;) SLA compliance, that will make my CEO and legal department happy :) Good security is a must of course.

HOW

I know you want to learn to use this so shall we? We will:

  • Provision an Azure SignalR service
  • Create an Azure Function app, that will allow us to connect to the Azure SignalR Service. We will learn how to manage connections and also how to receive and send messages.
  • Create a UI that is able to connect to our Azure Function App and send/receive messages.

Provision an Azure SignalR Service

  1. Go to portal.azure.com

  2. Click + Create a resource

  3. Enter SignalR Service in the search field

  1. Press Review + Create and then Create on the next screen.

NOTE, One last step. We need to set up our Azure SignalR service so that it can communicate with Serverless apps, otherwise the handshake, when connecting, will fail. I learned that the hard way :)

Create Azure Function App

This involves us creating an Azure Function app. It will have two different functions in it:

  • negotiate, this will talk to our Azure SignalR service and give back an API key that we can use when we want to do things like sending messages
  • messages, this endpoint will be used to send messages

Pre requisites

First off, as with any Azure Function we need to ensure we have installed the prerequisites which look different on different OSs:

For Mac:

brew tap azure/functions
brew install azure-functions-core-tools

For Windows:

npm install -g azure-functions-core-tools

Read more here if you have Linux as OS:

https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local

One more thing, to make authoring a Serverless Function I recommend installing the Azure Function extension. This will enable you to scaffold functions as well as debugging and deploying them. Go to your extension tab in VS Code and install the below:

If you are on Visual Studio, have a look here:

https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs

Create our Serverless functions

Ok then, for the sake of this article we will be using VS Code as our IDE of choice. We will do the following:

  • Create an Azure Function App, an Azure Function needs to belong to an app
  • Scaffold two Azure Functions, negotiate and messages
  • Configure our two functions to work with our Azure SignalR service

Bring up the command palette View/Command Palette, or CMD+SHIFT+P on a Mac.

Next, select a directory for your app (I usually pick the one I'm standing in)

After that, we are asked to select a language. As you can see below we have quite a few options. Let's go with C# for this one.

The next step is to select a Trigger for your first function (first time when you create a Serverless project it will create project + one function). A Trigger determines how our function will be started. In this case, we want it to be started/triggered by an HTTP call so we select HttpTrigger below:

We have two more steps here, those are:

  • Name of our function, let's call it negotiate
  • Namespace, call it Company
  • Authorization let's go with Anonymous

Ok, so now we have gotten a Serverless .NET Core project. Let's bring up the command palette once more View/Command Palette and enter Azure Functions: Create Function like the below.

Select:

  • Trigger select HttpTrigger
  • Function name, call it messages
  • Namespace call it Company
  • Authorization level, let's select anonymous

Ok, then, we should at this point have a create a Function app/Function Project with two functions in it. It should look like this, after you renamed negotiate.cs to Negotiate.cs and messages.cs have been renamed to Messages.cs:

Configure SignalR

At this point we need to do two things:

  • Add SignalR decorators in code, this ensures we are connecting to the correct Azure SignalR instance in the Cloud
  • Add Connection String information, we need to add this information to our config file so it knows what SignalR instance to talk to

Add SignalR decorators

Let's open up Negotiate.cs and give it the following code:

// Negotiate.cs
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;

namespace Company
{
  public static class Negotiate
  {
      [FunctionName("negotiate")]
      public static SignalRConnectionInfo GetSignalRInfo(
          [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
          [SignalRConnectionInfo(HubName = "chat")] SignalRConnectionInfo connectionInfo)
      {
          return connectionInfo;
      }
  }
}

From the above code, we can see that we have the decorator SignalRConnectionInfo and we point out a so-called hub called chat. Additionally, we see that the function ends up returning a connectionInfo object. What goes on here is that when this endpoint is being hit by an HTTP request we handshake with our Azure SignalR Service in the Cloud and it ends up giving us the needed connection info back so we can keep talking it when doing things like sending messages.

Now let's open Messages.cs and give it the following code:

// Messages.cs
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;

namespace Company
{
  public static class Messages
  {

    [FunctionName("messages")]
    public static Task SendMessage(
          [HttpTrigger(AuthorizationLevel.Anonymous, "post")] object message,
          [SignalR(HubName = "chat")] IAsyncCollector<SignalRMessage> signalRMessages)
    {
      return signalRMessages.AddAsync(
          new SignalRMessage
          {
            Target = "newMessage",
            Arguments = new[] { message }
          });
    }
  }
}

This time around we also use a decorator, but it's called SignalR but we still give it the Hub value chat. Our SignalR decorator decorates a list of messages that has the parameter name signalRMessages.

Let's have a look at the function body next. We see that we call signalRMessages.AddAsync(). What does that do? Well, it passes in SignalRMessage that consists of two things:

  • Target, this is the name of an event, in this case, newMessage. A client can listen to this event and render its payload for example
  • Arguments, this is simply the payload, in this case, we just want to broadcast all messages that come from one client, to ensure other listening clients would be updated on the fact that there is new data.

Add Connection String

Ok, so we learned that our code needs SignalR decorators in the code to work properly. Nothing will work however unless we add the Connection String information to our project configuration file called local.setting.json.

Let's have a look at the current state of the file:

{
    "IsEncrypted": false,
    "Values": {
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "AzureSignalRConnectionString": "<add connection string info here>"
    },
    "Host": {
        "LocalHttpPort": 7071,
        "CORS": "<add allowed client domains here>",
        "CORSCredentials": true
    }
}

Let's look at AzureSignalRConnectionString , this needs to have the correct Connection String info. We can find that if we go our Azure SignalR Service in the Cloud.

  1. Go to portal.azure.com
  2. Select your Azure SignalR Service
  3. Click keys in the left menu
  4. Copy the value under CONNECTION STRING

Next, let's update the CORS property. Because we are running this locally we need to allow, for now, that http://localhost:8080 is allowed to talk our Azure Function App and Azure SignalR Service.

NOTE, we will ensure that the client we are about to create will be run on port 8080.

Create a UI

Ok, we've taken all the necessary steps to create a backend, and an Azure SignalR service that is able to scale our real-time connections. We've also added a serverless function that is able to proxy any calls made to our Azure SignalR service. What remains is the application code, the part our users will see.

We will build a chat application. So our app will be able to do the following:

  • Establish a connection to our Azure SignalR Service
  • Show incoming messages from other clients
  • Send messages to other clients

Establish a connection

Let's select a different directory than that of our serverless app. Now create a file index.html and give it the following content:

<html>
  <body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.1.2/dist/browser/signalr.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
    <script>

    </script>
  </body>
</html>

Above we have added some script tags:

  • Vue.js, this is a link to a CDN version on Vue.js, you can go with whatever SPA framework you want here or Vanilla JS
  • SignalR, this is a link to a CDN version of SignalR, this is a must, we need this to establish a connection to our SignalR Hub and also for sending messages that other clients can listen to
  • Axios, this is a link to a CDN version of Axios, Axios is a library for handling HTTP requests. You are fine using the native fetch in this case, up to you

How do we establish a connection in code? The code below will do just that. We point apiBaseUrl to the location of our Serverless Function App, once it's up and running.

const apiBaseUrl = 'http://localhost:7071';

const connection = new signalR.HubConnectionBuilder()
    .withUrl(`${apiBaseUrl}/api`)
    .configureLogging(signalR.LogLevel.Information)
    .build();

The above will set up a connection object. To actually connect we need to call start() on our connection object.

console.log('connecting...');
connection.start()
  .then((response) => {
    console.log('connection established', response);
  })
  .catch(logError);

Before we move on let's try to verify that we can connect to our Serverless function and the Azure SignalR service.

Take it for a spin

We need to take the following steps to test things out:

  1. Startup our Serverless function in Debug mode
  2. Startup our client on http://localhost:8080
  3. Ensure the connection established message is shown in the client

Go to our Serverless app and select Debug/Start Debugging from the menu. It should look like the below.

Also, place a breakpoint in Negotiate.cs and the first line of the function, so we can capture when the client is trying to connect.

Next, let's start up the client at http://localhost:8080. Use for example http-server for that at the root of your client code:

http-server -p 8080

As soon as you go open up a browser on http://localhost:8080 it should hit your Serverless function negotiate, like so:

As you can see above, the Azure SignalR service is sending back an AccessToken and the URL you were connecting against.

Looking at the browser, we should see something like this:

Good, everything works so far. This was the hard part. So what remains is building this out to an app that the user wants to use, so that's next. :)

 Build our Vue.js app

Our app should support:

  • Connecting to Azure SignalR Service, we got that down already
  • Show messages, be able to show messages from other clients
  • Send message, the user should be able to send a message

Let's get to work :)

Create a Vue.js app

We need to create a Vue app and ensure it renders on a specific DOM element, like so:

<html>
  <body>
    <div id="app">
      App goes here
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.1.2/dist/browser/signalr.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
    <script>
      const app = new Vue({
        el: '#app',    
      });

      const apiBaseUrl = 'http://localhost:7071';

      const connection = new signalR.HubConnectionBuilder()
        .withUrl(`${apiBaseUrl}/api`)
        .configureLogging(signalR.LogLevel.Information)
        .build();

      console.log('connecting...');
      connection.start()
        .then((response) => {
          console.log('connection established', response);
      })
        .catch(logError);


    </script>
  </body>
</html>

Above we have the entire code so far. Let's specifically highlight:

<div id ="app">
</div>

and

const app = new Vue({
  el: '#app',    
});

Now we have an app, but it does nothing.

Show messages

To be able to show messages we need to listen to events being raised from our Serverless function. If you remember, in our Serverless function we called the following code in our Messages.cs:

return signalRMessages.AddAsync(
  new SignalRMessage
  {
    Target = "newMessage",
    Arguments = new[] { message }
  });

We are interested in listening to the event newMessage being raised by the above function. The code for that looks like so:

connection.on('newMessage', newMessage);

function newMessage(message) {
  // do something with an incoming message
}

Ok, how do I get that message to show in my Vue.js app?

Let's make sure to update our markup to this:

<div id="app">
  <h2>Messages</h2>
  <div v-for="message in messages">
    <strong>{{message.sender}}</strong> {{message.text}}
  </div>
</div>

and our app code to:

const data = {
  messages: []
}

const app = new Vue({
  el: '#app',    
  data: data
});

and this:

function newMessage(message) {
  data.messages = [...data.messages, {...message}]
}

Now we can render all the messages.

But there are no messages to show and I'm the only person in the chat room and I can't send a message?

Fair point, let's give you that ability:

Send message

We need a way for the user to type in a message in HTML and also a way to send that message to the SignalR Hub in code. Let's start with the HTML

<div>
  <input type="text" v-model="newMessage" id="message-box" class="form-control"
    placeholder="Type message here..." autocomplete="off" />
  <button @click="sendMessage">Send message</button>
</div>

and the code for the send function:

function createMessage(sender, messageText) {
  return axios.post(`${apiBaseUrl}/api/messages`, {
    sender: sender,
    text: messageText
  }).then(resp => console.log('success sending message',resp.data);
}

Our full code so far looks like this:

<html>
  <body>
    <div id="app">
      <h2>
        User
      </h2>
      <div>
        <input type="text" v-model="user" placeholder="user name" />
      </div>
      <div>
          <input type="text" v-model="newMessage" id="message-box" class="form-control"
            placeholder="Type message here..." autocomplete="off" />
        <button @click="sendMessage">Send message</button>
      </div>
      <h2>Messages</h2>
      <div v-for="message in messages">
        <strong>{{message.sender}}</strong> {{message.text}}
      </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.1.2/dist/browser/signalr.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
    <script>
      const data = {
        user: 'change me',
        messages: [],
        newMessage: ''
      }

      const app = new Vue({
        el: '#app',    
        data: data,
        methods: {
          sendMessage() {
            createMessage(this.user, this.newMessage);
          }
        }
      });

      const apiBaseUrl = 'http://localhost:7071';

      const connection = new signalR.HubConnectionBuilder()
        .withUrl(`${apiBaseUrl}/api`)
        .configureLogging(signalR.LogLevel.Information)
        .build();

      console.log('connecting...');
      connection.start()
        .then((response) => {
          console.log('connection established', response);
      })
        .catch(logError);

      connection.on('newMessage', newMessage);

      function newMessage(message) {
        data.messages = [...data.messages, {...message}]
      }

      function logError(err) {
        console.error('Error establishing connection', err);
      }

      function createMessage(sender, messageText) {
        return axios.post(`${apiBaseUrl}/api/messages`, {
          sender: sender,
          text: messageText
        }).then(resp => {
          console.log('message sent', resp);
        });
      }

    </script>
  </body>
</html>

and running this with two different windows side by side should look something like this:

As you can see this works pretty well but it ain't pretty so feel free to add Bootstrap, Bulma, Animations or whatever else you feel is needed to make it a great app.

Summary

We've learned the following:

  • SignalR, what it is and how it can be hosted either as part of your Web App in App Service or via an Azure SignalR Service + Serverless
  • Serverless, we've taken our first steps in serverless and learned how to scaffold an app with functions
  • Chat, we've learned how to build a Chat by Creating a Serverless app as an endpoint and we've also built a Client in Vue.js

Want to submit your solution to this challenge?


Want to submit your solution to this challenge? Build a solution locally and then submit an issue. If your solution doesn't involve code you can record a short video and submit it as a link in the issue description. Make sure to tell us which challenge the solution is for. We're excited to see what you build! Do you have comments or questions? Add them to the comments area below.


Watch for surprises all during December as we celebrate 25 Days of Serverless. Stay tuned here on dev.to as we feature challenges and solutions! Sign up for a free account on Azure to get ready for the challenges!

Oldest comments (16)

Collapse
 
sumitkharche profile image
Sumit Kharche

Great article!

Collapse
 
softchris profile image
Chris Noring

Thank you :)

Collapse
 
troywitthoeft profile image
Troy Witthoeft

Very nice. I've been wanting to try SignalR and Vue for awhile and this article is so well-written and easy to follow, I am going to dive in. Thanks!

Collapse
 
softchris profile image
Chris Noring

hi Troy. Thanks for that. Let me know if you have any questions :)

Collapse
 
xfischer profile image
Xavier Fischer • Edited

Hi Chris and thanks for sharing this.
Can you elaborate on costs? This one of the main issues I've had with Azure. Yes, it can scale, if you follow the rules, and build a nearly Azure dedicated architecture.

But when it will actually ramp up to the peak of "1 million chat users", how much will that really cost if we build this app and make it go live on Azure ?

Collapse
 
anthony profile image
Anthony Chu

It costs about $2/day per 1000 concurrent SignalR service connections. Generally if you have 1 million users, they are not connected at once. For Functions... it costs one execution for a function to send a message via SignalR Service, no matter how many devices are connected.

Collapse
 
xfischer profile image
Xavier Fischer • Edited

I did the maths to check, I may be wrong, please correct me if so.
I have used the Azure Pricing calculator.(azure.microsoft.com/en-us/pricing/...)

Let's imagine a real case where 1 million users would connect to and send only 1 message to test the chat room on a monthly basis.

  • Azure Functions cost : 0$ because the first million is free. Even if they send 10 messages, it's still cheap (2$ as you said)

Now, SignalR :

  • The free tier does not apply because it allows only 20 simultaneous connections
  • Standard tier starts at 41,30 € (max 1 million messages, max 1000 simultaneous connections per unit) (let's say 40$)

If we sum up, for a chat room like this (which is very well done and neat, Azure is great, no doubt on that) we have 42$/month.

And as for the 1000 users/day, that's only within the limit of approx 32 messages/user/day on a monthly basis. Pass that limit and you'll need two SignalR units, doubling the price.

Moreover:
If you have more than 1000 simultaneous users, how do you plug new ones to another unit ? This would double the messages sent ?

Am I wrong ?

Thread Thread
 
softchris profile image
Chris Noring

Hi Xavier. I've sent your question to the product team. Hope to get back to you soon :)

Thread Thread
 
xfischer profile image
Xavier Fischer

Hi Chris, so how did it go with the product team ?

Thread Thread
 
softchris profile image
Chris Noring

hi Xavier. I believe Anthony's first response is accurate. We are working on revamping the page for doing calculations on Cloud costs. Hopefully, this will become easier to use quite soon

Thread Thread
 
xfischer profile image
Xavier Fischer • Edited

So we have 60$/month on a 1000 users/day average on a monthly basis, based on Anthony's statement.

Please keep me and readers an posterity informed:

  • when the Azure pricing calculator will be "revamped" and will accurately evaluate costs (I can't believe the M of GAFAM shouldn't get this fixed within a couple days)
  • when you will be able to give a clear answer to the costs involved, as precise as the shiny tech bits delivered.

And one last question: Is really al this about technology or is it marketing ? If so, I promess I won't ask anything anymore.

Thread Thread
 
htissink profile image
Henrick Tissink

Really wish Chris finished off the thread - dealing with similar issues with signalr at the moment.

Thread Thread
 
xfischer profile image
Xavier Fischer

Cost issues ?

Thread Thread
 
htissink profile image
Henrick Tissink

wow, appreciate you replying 2 years after the comment :)

If you have more than 1000 simultaneous users, how do you plug new ones to another unit ? This would double the messages sent ?

mainly around that. We currently have a bunch of signalr pods on a single node, with a redis backplane on that same node, doing around 1 billion messages a month - imagine it's a stock trading app with price updates - but we're at a loss for scaling this out without a backplane, especially signalr onto other nodes. Have you solved an issue similar to ours? I would absolutely love just plugging into an azure service and making that headache an MS problem :P but it seems we're going to have to deal with this one on our own.

Thread Thread
 
xfischer profile image
Xavier Fischer

I was notified ;) I'm not the author, and no author replied to my questions neither. Seems that your problem is quite different : I was originally stating that omitting the real cost from the discussion was a bit unfair to the audience (ie marketing-like sponsored posts).
You seem to have the $$ so you may get an answer :)

Thread Thread
 
htissink profile image
Henrick Tissink

That is the peculiar thing lol, we do, and we really don't want to deal with this problem - but I think it's inevitable :) and I wholeheartedly agree, the marketing-like posts are all fun and jazzy but conveniently step over a lot of the pitfalls of this stuff, especially cost. MS page advises how to scale out with redis, cautions against it (in our case) but never really offers a good alternative.