DEV Community

Cover image for How to build a performant SignalR chat SPA using Angular 10 and .Net Core 3.1
Javier Acrich
Javier Acrich

Posted on • Edited on

How to build a performant SignalR chat SPA using Angular 10 and .Net Core 3.1

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.

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

Protocols

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):

Transports

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

Web API Net Core Solution

Now that we have a solution, let's install the following required nugets:

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);
    }


Enter fullscreen mode Exit fullscreen mode

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; }
    }


Enter fullscreen mode Exit fullscreen mode

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();
                });
            });
        }


Enter fullscreen mode Exit fullscreen mode

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");
            });


Enter fullscreen mode Exit fullscreen mode

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 
        }
    }


Enter fullscreen mode Exit fullscreen mode

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

CHAT UI

Write the following CLI commands to create the UI for the chat app

  1. ng new chat-ui in any folder to scaffold and create a new angular app
  2. npm i @microsoft/signalr@latest --save to install the required SignalR libraries.
  3. ng g s signalr to scaffold the SignalR service
  4. 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")
    })
  }
}



Enter fullscreen mode Exit fullscreen mode

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.

calling the hub directly

B. We can send Http requests to the API Controller and let the controller call the hub.

calling the hub thru the API controller

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)
    });
  }
}


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
weirdyang profile image
weirdyang

Do you think using ngzone runOutsideAngular to handle signalr events and invokes will help with performance?