More and more companies are using web application to provide their services. Building those you will face many common problems. One of them is the communication between different clients of the same web application, for example used for instant messaging or simultaneous editing in the browser, like done in Google Drive or Microsoft Office for the web . The general aim is updating information on one client when some changes are done on another client.
As usual, there are many ways to solve this problem. In this example we will use the stack of .NET 5 for the backend and Angular for the frontend, which allows us to easily use SignalR for the cross-client communication.
Real-time updating with SignalR
SignalR is an open source project by Microsoft and can easily be integrated in a .NET application. Additionally, there is a npm package, which allows you to integrate it into your web frontend, for example your React or Angular application.
What does SignalR mainly do? It provides functionality to push updates from the server to the client, so information can be updated without refreshing the page. This makes your web application real-time ready.
How does SignalR achieve this? It tries to use different technologies for the communication depending on the support by client and server. So you don’t need to care about WebSockets, Polling or Server Sent Events, as SignalR already does.
Our example - the Quizzer app
Let's build a real-time web application from scratch. As an example we take a Quiz app with a quiz master and several quiz candidates. Each of them will be represented by a browser instance. If the quiz master starts the next question, it should be pushed to all the candidates' clients. If a candidate makes a guess, the quiz master should be informed. And of course, if a candidate joins the quiz, they should set a name and notify the quiz master about this. So in this example we do not just want to push updates from the server to the clients, but push updates from one client to the other clients. As we can't do this directly because the clients don't know of each others, we do this cross-client communication with the help of the server.
Prerequisites
To follow this tutorial and build the app yourself, you need to have some things installed on your machine:
- .NET 5 SDK and runtime
- Visual Studio 2019 or higher
- node.js v16 or higher with npm v8 or higher
- Angular v13 or higher
You can find the complete source code here.
Getting started with the .NET 5 backend
First we need to create our backend in Visual Studio. Hit "Create new project" and choose "ASP.NET Core-Web-API" as the project template. It will create a project with the standard setup for a Web API and a controller, which is exactly what we need.
In order to use SignalR in our backend, we need to enable it in the Startup.cs as follows:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddSignalR();
// ...
}
As we are already here, we can also do another thing which will enable local debugging of frontend and backend together. We configure CORS to allow localhost as an origin:
public class Startup
{
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins, builder =>
{
builder
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.WithOrigins("http://localhost:4200")
.Build();
});
});
// ...
}
// ...
}
We allow localhost at port 4200 as an origin because this is where our Angular application will run by default. So we ensure that API calls by the frontend won’t be blocked.
One central element of SignalR is the so called Hub. We’ll come to the details later, but we already register our QuizHub in the Startup.cs by using the MapHub() function:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<QuizHub>("/hub");
});
}
Working with the SignalR Hub
As we just registered our QuizHub, of course we need to implement it. SignalR provides an abstract class called Hub and we need to inherit from this class:
using Microsoft.AspNetCore.SignalR;
public class QuizHub : Hub
{
}
In this class we will now add methods that can be called by the clients:
public async Task MakeGuess(string player, string answer)
{
await this.Clients.All.SendAsync("GuessMade", player, answer);
}
public async Task SetPlayerName(string playerName)
{
await this.Clients.All.SendAsync("PlayerNameSet", playerName);
}
If a client calls one of these methods, all other clients that listen to the specific event (GuessMade or PlayerNameSet) will be informed and get the data passed by the calling client. We will see how we can actually do those calls when we come to the frontend part.
Getting the quiz data
Let’s care for questions and answers now, so we can properly test our example later. You could build your own quiz data in a database, but for this example we use the Open Trivia DB which provides us with thousands of questions. We can simply retrieve the data via their REST API. In the query we can specify the amount, type and difficulty of the questions we want to get back, so the API call
https://opentdb.com/api.php?amount=1&difficulty=medium&type=multiple
will return one multiple choice question with medium difficulty.
The process of getting the question is implemented in Data/QuestionProvider.cs. In the Data folder, you can also find two model classes which help deserialize the API response.
The whole retrieval of the data is then encapsulated in QuizQuestionRepository.cs which provides only one method GetNextQuestion() that can be used in the controller.
Setting up the QuizController
The QuizController provides a method for the quiz master to go to the next question. Apart from the QuizHub itself, we can also send events to all listening clients in the controller. To do so, we need to get an instance of IHubContext from the QuizHub via dependency injection:
public QuizController(IHubContext<QuizHub> hubContext)
{
_hubContext = hubContext;
}
Once we have this IHubContext, we can use it to call the same method as we do in the QuizHub and populate the new question:
[HttpGet("Next")]
public async Task NextQuestion()
{
var question = await this.repository.GetNextQuestion();
await _hubContext.Clients.All.SendAsync("GoToQuestion", question);
}
And that’s all we need from the backend side of things. Let’s move on to the frontend.
Creating the Angular application
Open Visual Studio Code and type ng new QuizzerFrontend
in the terminal to get a fresh Angular setup. We need to create three components. One HomeComponent to choose if the client is the quiz master or a candidate and the QuizMasterComponent and QuizCandidateComponent for each of them. For the details of these components please visit the source code on GitHub.
We also need quiz.service.ts to interact with the backend and call the endpoint provided by our QuizController. Here we implement the API call:
nextQuestion(): Subscription {
return this.http
.get(this.url + 'Next')
.pipe(catchError(this.handleError)).subscribe();
}
Implementing the SignalR communication in the frontend
Let’s rush to the interesting parts now! We create a new service signalr.service.ts to manage all the communication through SignalR. To be able to do so, we need to install the SignalR package by executing npm install @microsoft/signalr
.
From this package we use the HubConnectionBuilder first to create a HubConnection:
private buildConnection(): HubConnection {
return new HubConnectionBuilder()
.withUrl(this.url)
.withAutomaticReconnect()
.build();
}
The url is in this case the backend url, where we configured our QuizHub earlier in the Startup.cs, i.e. https://localhost:44375/hub/. Once that’s done, we need to register on the events, our client should listen to:
private registerOnServerEvents(): void {
this.hubConnection.on('GoToQuestion', (question: any) => {
this.questionReceived$.next(question);
});
this.hubConnection.on('GuessMade', (player: string, guess: string) => {
this.guessMade$.next({ player, guess });
});
this.hubConnection.on('PlayerNameSet', (playerName: string) => {
this.setPlayerName$.next(playerName);
});
}
At last we need to start the connection by calling start()
on our HubConnection instance. The SignalRService is used in the components, which take their actions on the Subjects that populate the events sent by the SignalR hub, so they can properly react on those events.
And this is all, we have created a Quizzer app with cross-client communication to let our quiz master and candidates interact!
Just download the source code and try it out!
This article was initially published at https://from-scratch.de.
Top comments (0)