Go back

How to Build a Performant SignalR Chat SPA

SignalR


Before we get started, you will need the source code for this application, which can be found at the end of the text.Now, let’s start exploring the concepts of SignalR and try to explain, simply, how to build a chat interface in a single page application.

SignalR Definitions

ASP.NET Core SignalR is an open-source library that simplifies apps by adding real-time web functionality. Real-time web functionality enables server-side code to push content to clients instantly.Let’s take a look at some of the
most important concepts in SignalR: Hubs, Protocols, and Transports.

Hub

A hub is the intermediary between the client and the server.
SignalR chat SPA
SignalR uses hubs to communicate between clients and servers. Hubs are high-level pipelines that allow a client and server to call methods on each other.SignalR handles the dispatching across machine boundaries automatically, allowing clients to call methods on the server and vice versa.

Protocols

If you have got this far it’s important to know that there
are 2 protocols which can be used in SignalR.
Protocols
These are serialization formats which can be used to send messages between the server and client.
JSON is the default protocol, but we can also use MessagePack, which is a fast and compact binary serialization format. It is useful when performance and bandwidth are a concern because it creates smaller messages compared to  JSON. The binary messages are unreadable when looking at network traces and logs unless the bytes are passed through a MessagePack parser. SignalR has built-in support for the MessagePack format and provides APIs for the client and server to use.

Transports

SignalR supports the following techniques
for handling real-time communication (in order of graceful fallback):
Web sockets
WebSockets performs best, but can only be used if both the client and the server support it. If that is not the case, SSE or Long Polling should be used instead.

Backend

Let’s start by creating a blank web API .Net Core 3.1 Solution in visual studio.
backend Signal
Now that we have a solution, let’s install the following required nugets:
Packages SignalR
The first one is the core NuGet required for SignalR, and the second is required to use the MessagePack protocol. Now let’s create a ChatHub class and the IChatHub interface that will represent the proxy between the client and server.

public class ChatHub : Hub
{
public async Task BroadcastAsync(ChatMessage message)
{
await Clients.All.MessageReceivedFromHub(message);
}
public override async Task OnConnectedAsync()
{
await Clients.All.NewUserConnected("a new user connectd");
}
}

public interface IChatHub
{
Task MessageReceivedFromHub(ChatMessage message);

Task NewUserConnected(string message);
}

As you can see the ChatMessage class will be a simple Data Transfer Object that will be used to transfer the messages.

public class ChatMessage
{
public string Text { get; set; }
public string ConnectionId { get; set; }
public DateTime DateTime { get; set; }
}

Now, we need to tell Asp.Net Core we want to use SignalR, registering the services in the Configure Services Method. 

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();

services.AddSignalR().AddMessagePackProtocol();

services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder
.WithOrigins(
"http://localhost")
.AllowCredentials()
.AllowAnyHeader()
.SetIsOriginAllowed(_ => true)
.AllowAnyMethod();
});
});
}

Take a look at the CORS default policy, we are also going to need it since we are building an SPA.Now, let’s map the ChatHub class we previously wrote in the Configure method.

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub("/signalr");
});

And to finish the server part, let’s write the ChatController.

[Route("api/[controller]")]
[ApiController]
public class ChatController : ControllerBase
{
private readonly IHubContext hubContext;

public ChatController(IHubContext hubContext)
{
this.hubContext = hubContext;
}

[HttpPost]
public async Task SendMessage(ChatMessage message)
{
//additional business logic

await this.hubContext.Clients.All.SendAsync("messageReceivedFromApi", message);

//additional business logic
}
}

Pay close attention to the dependency injected in the controller constructor. It is not a _ChatHub_, It is an _IHubContext_. This is the correct way of injecting a Hub Reference in a controller according to SignalR docs. We can use this hub reference to call hub methods from outside the hub.It is important to mention here that although we are injecting a Hub reference in the controller, it is not actually necessary. As discussed below, this is only required if you need to execute additional logic in the controller before or after calling the hub.

If you want extra config options, take a look at this doc.

Frontend

We will use a simple Angular app without much styling effort since we are only interested in demonstrating how SignalR works. You can see the UI in the following image.
SignalR Chat
Write the following CLI commands to create the UI for the chat app.

  • *_ng new chat-ui_* in any folder to scaffold and create a new angular app
  • *_npm i @microsoft/signalr@latest –save_* to install the required SignalR libraries.
  • *_ng g s signalr_* to scaffold the SignalR service
  • *_npm i @microsoft/signalr-protocol-msgpack –save_* to use the MessagePack protocol in the frontend

First, let’s write the SignalR service which will be used from the UI Component.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'
import { from } from 'rxjs';
import { tap } from 'rxjs/operators';
import { chatMesage } from './chatMesage';
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'

@Injectable({
providedIn: 'root'
})
export class SignalrService {

private hubConnection: HubConnection
public messages: chatMesage[] = [];
private connectionUrl = 'https://localhost:44319/signalr';
private apiUrl = 'https://localhost:44319/api/chat';

constructor(private http: HttpClient) { }

public connect = () => {
this.startConnection();
this.addListeners();
}

public sendMessageToApi(message: string) {
return this.http.post(this.apiUrl, this.buildChatMessage(message))
.pipe(tap(_ => console.log("message sucessfully sent to api controller")));
}

public sendMessageToHub(message: string) {
var promise = this.hubConnection.invoke("BroadcastAsync", this.buildChatMessage(message))
.then(() => { console.log('message sent successfully to hub'); })
.catch((err) => console.log('error while sending a message to hub: ' + err));

return from(promise);
}

private getConnection(): HubConnection {
return new HubConnectionBuilder()
.withUrl(this.connectionUrl)
.withHubProtocol(new MessagePackHubProtocol())
// .configureLogging(LogLevel.Trace)
.build();
}

private buildChatMessage(message: string): chatMesage {
return {
connectionId: this.hubConnection.connectionId,
text: message,
dateTime: new Date()
};
}

private startConnection() {
this.hubConnection = this.getConnection();

this.hubConnection.start()
.then(() => console.log('connection started'))
.catch((err) => console.log('error while establishing signalr connection: ' + err))
}

private addListeners() {
this.hubConnection.on("messageReceivedFromApi", (data: chatMesage) => {
console.log("message received from API Controller")
this.messages.push(data);
})
this.hubConnection.on("messageReceivedFromHub", (data: chatMesage) => {
console.log("message received from Hub")
this.messages.push(data);
})
this.hubConnection.on("newUserConnected", _ => {
console.log("new user connected")
})
}
}

*connect()* is the first method. This method will be called from OnInit() in the UI component. This method is responsible for connecting to the ChatHub and registering the listeners that will be listening when the server tries to send messages to the client.In this case, and since we are in an SPA, we have 2 options to send messages from the UI to the HUB:A –  We can send messages directly to the  HUB.
Call the hub
B – We can send Http requests to the API Controller and let the controller call the hub.
Api controller
Either option is fine and the choice depends on your requirements. If you need to execute additional business rules besides sending the message,
it would be a good idea to call the API controller and execute your business rules there and not in the HUB,
just to separate concerns.

This is the Component in charge:

import { Component, OnInit } from '@angular/core';
import { SignalrService } from './signalr.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
title = 'chat-ui';
text: string = "";

constructor(public signalRService: SignalrService) {

}

ngOnInit(): void {
this.signalRService.connect();
}

sendMessage(): void {
// this.signalRService.sendMessageToApi(this.text).subscribe({
// next: _ => this.text = '',
// error: (err) => console.error(err)
// });

this.signalRService.sendMessageToHub(this.text).subscribe({
next: _ => this.text = '',
error: (err) => console.error(err)
});
}
}

We can see here that the connect() method is called on OnInit() to initialize the connection.The SendMessage() method is called whenever the user clicks the Send Button. For demo purposes, you have both options.Sending the message directly to the hub and sending the message through a request to an action method in a controller.And finally, this is the markup with some bootstrap classes:
Code
If you want to take a look at the source code:

Javier A

About the Author

Javier Acrich a multi-talented .NET Developer capable of managing both front-end and back-end development. Successfully creates and modifies web features to improve functioning for clients. Skilled in architecture and design of web applications and solutions.