This article explores SignalR concepts and tries to explain with simplicity how to build a chat interface in a single page application. You can find the source code to this app at the end of this text.
ASP.NET Core SignalR is an open-source library that simplifies adding real-time web functionality to apps. Real-time web functionality enables server-side code to push content to clients instantly.
Let's see some of the most important concepts in SignalR: Hubs, Protocols, and Transports.
HUB
A hub is the thing in the middle between the client and the server.
SignalR uses hubs to communicate between clients and servers.
A hub is a high-level pipeline that allows 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
There are 2 protocols you can use in SignalR
These are serialization formats you can use to send messages between server and client. Json is the default protocol, but we can also use MessagePack which is a fast and compact binary serialization format. It's 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):
WebSockets has the best performance, but it is only available if both the client and the server support it. If that is not the case, SSE or Long Polling is used instead.
BACKEND
Let's start by creating a blank web API .Net Core 3.1 Solution in visual studio
Now that we have a solution, let's install the following required nugets:
The first one is the core NuGet required for SignalR, and the second will be 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<IChatHub>
{
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 ConfigureServices 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 that since this we are building a SPA.
Now, let's Map the ChatHub class we previously wrote in the Configure method
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<ChatHub>("/signalr");
});
And to finish the server part, let's write the ChatController
[Route("api/[controller]")]
[ApiController]
public class ChatController : ControllerBase
{
private readonly IHubContext<ChatHub> hubContext;
public ChatController(IHubContext<ChatHub> 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 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's important to say here that, although we are injecting a Hub reference in the controller, we don't actually need to. As we discuss 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 you can take a look at this doc.
FRONTEND
We’ll use a simple Angular app without much styling effort since we are only interested in showing how SignalR works. You can see the UI in the following image
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 the OnInit() in the UI component. This method is in charge of 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 a SPA, we have 2 options to send messages from the UI to the HUB:
A. We can send messages directly to the HUB.
B. We can send Http requests to the API Controller and let the controller call the hub.
Either option is fine and this depends on your requirements. If you need to execute additional business rules besides sending the message, it is 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 the OnInit() to initialize the connection.
The SendMessage() method is called whenever the user clicks the Send Button. Just for demo purposes, you have both options. Sending the message directly to the hub and sending the message thru a request to an action method in a controller.
and finally this is the markup with some bootstrap classes:
<div class="container mt-5">
<h1>SignalR Chat DEMO</h1>
<input type="text" class="mt-3 mb-3 mr-3" [(ngModel)]="text">
<button class="btn btn-primary" [disabled]="text.length==0" (click)="sendMessage()">Send Message</button>
<h4 class="mb-3">List of Messages</h4>
<div *ngIf="signalRService.messages.length==0">
<p>You haven't send or received messages</p>
</div>
<div *ngFor="let m of signalRService.messages">
<div class="mb-2 mt-2">
<div><strong>ConnectionID</strong> {{m.ConnectionId}}</div>
<div><strong>Date</strong> {{m.DateTime | date}}</div>
<div><strong>Message</strong> {{m.Text}}</div>
<hr>
</div>
</div>
</div>
If you want to take a look at the source code:
https://github.com/javiergacrich/chat-ui
https://github.com/javiergacrich/chat-server
My Name is Javier Acrich and I work as a software engineer @ Santex
Top comments (1)
Do you think using ngzone runOutsideAngular to handle signalr events and invokes will help with performance?