How to use SignalR with ASP.NET Core Angular

In this detailed guide, let's understand how to integrate SignalR with ASP.NET Core and Angular to build realtime web applications using an example.

What is SignalR?

Microsoft documentation defines SignalR as below –

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.

SignalR supports the following techniques for Realtime data transmission:

  • WebSockets
  • Server-Sent Events
  • Long Polling

SignalR automatically chooses the best transport method that is within the capabilities of the server and client.

SignalR with ASP.NET Core (.NET 6)

ASP.NET Core provides a very good integration with SignalR, it comes within the default AspNetCore.App package. In SignalR, you define Hubs – you think of them as a pipeline which allow client-server interaction.

The methods defined inside the Hubs can be called (or invoked) by the connected client, and the server can send data to the clients. We register SignalR as a service and then register the Hubs inside the Endpoint middleware. The Endpoint middleware also handles calls to the SignalR along with the API controller calls.

SignalR Client libraries are available for almost all languages and frameworks. You can pick the appropriate client package for your application and communicate with the Server side Hubs.

Let’s build an Example

To better understand how SignalR works, let’s build a simple Feed Server that constantly relays post feed to connected clients and then let’s build a client application in Angular that consumes this feed data. The server provides several “groups” to which clients can subscribe and receive respective feed from.

Think of it like a Reddit (kind of) 😉 application where you have many groups and you receive posts accordingly. We’ll leave the complexities and focus on the subscribe-publish mechanism in this explanation.

Hands on – SignalR with .NET 6 and Angular

Let’s begin by creating a new ASP.NET Core application which acts as a “FeedServer” – that pushes data to clients. I’m using .NET 6 as the target framework.

> dotnet new webapi --name FeedServer

Once the application is created, we’ll start by creating a Hub that is responsible for handling feed relays. I’ll call it FeedHub. It extends the Hub class that is provided by the SignalR package. For now I’ll leave the Hub empty, because I’m only pushing data from server to client for now.

using Microsoft.AspNetCore.SignalR;

namespace FeedServer.Hubs;

public class FeedHub : Hub
{
    public FeedHub()
    {
    }
}

I’ll then register this Hub as a Service and also add an Endpoint, so that .NET Core knows which Hub to be invoked when it receives connections from signalr clients.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSignalR();
builder.Services.AddLogging();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();
app.MapHub<FeedHub>("/feed");

app.Run();

Notice that apart from registering FeedHub, I have also added a CORS policy so that the Server can allow connection from the client – in this case, the Angular application that we’ll build next.

builder.Services.AddCors((options) =>
{
    options.AddPolicy("FeedClientApp",
        new CorsPolicyBuilder()
        .WithOrigins("http://localhost:4200")
        .AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials()
        .Build());
});

// other code and build the app

app.UseCors("FeedClientApp");

I’ve also added a Minimal API that returns the list of “groups” that are available for the user to choose to get feed from.

app.MapGet("/api/groups", () => Data.Groups).WithName("GetAllGroups");

Injecting SignalR into a Hosted Service

Finally, I will create a Hosted Service that generates and relays Feed over an interval. I will call it FeedService.cs;

It has a Timer set which invokes the method GenerateAndSendFeed() for every 5 seconds. Inside this method, I’ll create a Feed entity, that is randomly assigned a Group and an Author from a static list of Groups and Authors I maintain internally.

You can inject HubContext

You can inject a HubContext of a particular Hub into a HostedService. It is possible because we’re registering our custom Hub class as a service inside the .NET Core container.

In this case, I’m injecting the HubContext of FeedHub.

using System.Timers;
using FeedServer.Constants;
using FeedServer.Hubs;
using FeedServer.Models;
using Microsoft.AspNetCore.SignalR;

namespace FeedServer.Services;

public class FeedService : IHostedService
{
    private readonly IHubContext<FeedHub> _feedHubContext;
    private readonly ILogger<FeedService> _logger;
    private readonly System.Timers.Timer _timer;
    private readonly string[] _groups = Data.Groups;
    private readonly string[] _authors = Data.Authors;
    private readonly Random _randomGroup;

    public FeedService(
        IHubContext<FeedHub> feedHubContext, 
        ILogger<FeedService> logger)
    {
        _feedHubContext = feedHubContext;
        _logger = logger;
        _timer = new System.Timers.Timer(5000);
        _timer.Elapsed += GenerateAndSendFeed;
        _randomGroup = new Random(0);
    }

    private void GenerateAndSendFeed(
        object? sender, ElapsedEventArgs e)
    {
        // generate random feed object
        // groupName, postText, timestamp
        var feed = CreateFeedEntity(Guid.NewGuid().ToString());

        // send to all users registered to receive feed from group
        _feedHubContext.Clients.Group(feed.GroupName).SendAsync("GetGroupFeed", feed);

        // send to all users to receive feed
        _feedHubContext.Clients.All.SendAsync("GetFeed", feed);
    }

    private Feed CreateFeedEntity(string id)
    {
        var nextGroupIndex = _randomGroup.Next(_groups.Length - 1);
        var nextAuthorIndex = _randomGroup.Next(_authors.Length - 1);

        var author = 
          (nextAuthorIndex < _authors.Length ? _authors[nextAuthorIndex] : _authors[0]);
        
        var group = 
          (nextGroupIndex < _groups.Length ? _groups[nextGroupIndex] : _groups[0]);

        return new Feed
        {
            Id = id,
            PostText = $"Some random post created for {group} by {author} right now. Have a great day!",
            Created = DateTime.Now,
            Author = author,
            GroupName = group
        };
    }

    public Task StartAsync(
        CancellationToken cancellationToken)
    {
        _timer.Start();
        return Task.CompletedTask;
    }

    public Task StopAsync(
        CancellationToken cancellationToken)
    {
        _timer.Enabled = false;
        _timer.Dispose();
        return Task.CompletedTask;
    }
}

Ways to push data with SignalR

SignalR allows me to relay the information in the following ways, via the built-in properties within the IHubContext<FeedHub> interface.

  • To All the Connected Clients
    IHubContext<FeedHub>.Clients.All
  • To a Group of Connected Clients
    IHubContext<FeedHub>.Clients.Group(<string>GroupName)
  • To a particular client via ConnectionId
    IHubContext<FeedHub>.Clients.Client(<string>ConnectionId)

To send an entity to all the clients who are connected to the SignalR Hub, we call the below line of code –

// send to all users to receive feed
_feedHubContext.Clients.All.SendAsync("GetFeed", feed);

The string “GetFeed” is like an action method, which the clients will listen on. In this case, all the clients who are listening on “GetFeed” will receive this feed object.

Similarly, to send an entity to only those clients who are subscribed to a particular group, we call the below line of code –

// send to all users registered to receive feed from group
_feedHubContext.Clients.Group(feed.GroupName).SendAsync("GetGroupFeed", feed);

Similar to “GetFeed”, all clients who are listening on “GetGroupFeed” over a particular group will receive this feed object.

We can also send an entity particular to a client, by using the Clients.Client(“”).SendAsync() method in the same way as above. But it requires an active connectionId, which we can obtain from within the FeedHub class.

With this, the Server is now complete and it constantly relays feed to clients when they’re connected.

SignalR Client with Angular

As mentioned before, signalr library is available for almost all popular languages and frameworks – including Angular. To demonstrate, let’s start by building a client application that receives and binds this data from the Hub onto the UI.

I’ll start by creating an Angular application –

> ng new FeedApp --routing

Once the application is created, I’ll install the dependencies – first is SignalR Client and the next is Bootstrap, to make styling components easy.

> npm i @microsoft/signalr bootstrap@^4 jquery

Once installed, I’ll set the bootstrap CSS and JS files along with jQuery in angular JSON.

"builder": "@angular-devkit/build-angular:browser",
"options": {
  "outputPath": "dist/feed-app",
  "index": "src/index.html",
  "main": "src/main.ts",
  "polyfills": "src/polyfills.ts",
  "tsConfig": "tsconfig.app.json",
  "assets": [
    "src/favicon.ico",
    "src/assets"
  ],
  "styles": [
    "node_modules/bootstrap/dist/css/bootstrap.min.css",
    "src/styles.css"
  ],
  "scripts": [
    "node_modules/jquery/dist/jquery.min.js",
    "node_modules/bootstrap/dist/js/bootstrap.min.js"
  ]
},

Next, I’ll add a Service that encapsulates SignalR connectivity and subscriptions. I’ll name it signalr.service.ts; It looks like below –

import { Injectable } from '@angular/core';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import { Observable, Subject } from 'rxjs';
import { Feed } from '../models/feed';

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

  private hubConnection: any;

  public startConnection() {
    return new Promise((resolve, reject) => {
      this.hubConnection = new HubConnectionBuilder()
        .withUrl("https://localhost:7023/feed").build();
        
      this.hubConnection.start()
        .then(() => {
          console.log("connection established");
          return resolve(true);
        })
        .catch((err: any) => {
          console.log("error occured" + err);
          reject(err);
        });
    });
  }

  constructor() { }
}

Notice that I’ve provided the Server address and the route prefix “/feed” which I have used while Mapping my Hub within the Server. We’ll call startConnection() method whenever we’re going to subscribe to any signalr feed.

Subscribe to ALL messages from Hub

To receive messages that the FeedServer pushes to “ALL” the connected clients, we’ll subscribe to the “GetFeed” action on which the FeedServer relays to all the clients. I’ll add the below lines of code to the signalr.service.ts class –

private $allFeed: Subject<Feed> = new Subject<Feed>();

public get AllFeedObservable(): Observable<Feed> {
  return this.$allFeed.asObservable();
}

public listenToAllFeeds() {
  (<HubConnection>this.hubConnection).on("GetFeed", (data: Feed) => {
    this.$allFeed.next(data);
  });
}

I’ll create a component Home, where I’ll subscribe to this feed and bind it to the View. The HomeComponent class looks like this –

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { Feed } from '../models/feed';
import { SignalrService } from '../services/signalr.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit, OnDestroy {
  feed: Feed[] = [];
  allFeedSubscription: any;

  constructor(private signalrService: SignalrService) { }

  ngOnInit(): void {
      // 1 - start a connection
    this.signalrService.startConnection().then(() => {
      console.log("connected");

      // 2 - register for ALL relay
      this.signalrService.listenToAllFeeds();

      // 3 - subscribe to messages received
      this.allFeedSubscription = this.signalrService.AllFeedObservable
            .subscribe((res: Feed) => {
              this.feed.push(res);
            });
    });
  }

  ngOnDestroy(): void {
    (<Subscription>this.allFeedSubscription).unsubscribe();
  }
}
<ng-container *ngIf="feed.length > 0; else feedEmpty">
    <div *ngFor="let f of feed" class="card rounded-0 my-2">
        <div class="card-header bg-transparent">
            <p class="card-text">
              {{ f.author }} posted in 
              <a [routerLink]="['/groups',f.groupName]">{{ f.groupName }}</a> 
              on {{ f.created | date:'MM/dd/yyyy' }}</p>
        </div>
        <div class="card-body">
            <p class="card-text">{{ f.postText }}</p>
        </div>
    </div>
</ng-container>
<ng-template #feedEmpty>
    <p>No Feed yet.</p>
</ng-template>

Receive messages from Hub Group

To receive messages that are relayed to only a particular group, we’ll have to first register to that group and then subscribe to the action on which the Hub relays to clients in that group.

To do this, I’ll add a method inside the FeedHub.cs class that extends the Hub class. Here I’ll add a parameter groupName and when this method is called, I’ll add the currently active connectionId to the group.

public class FeedHub : Hub
{
    public FeedHub()
    {
    }

    public async Task RegisterForFeed(string groupName)
    {
        await this.Groups.AddToGroupAsync(
            this.Context.ConnectionId, groupName);
    }
}

In the FeedService.cs HostedService, I’m also relaying the messages created to the appropriate groups with the line of code below –

// send to all users registered to receive feed from group
_feedHubContext
  .Clients
  .Group(feed.GroupName)
  .SendAsync("GetGroupFeed", feed);

On the client side, I’ll have to follow two steps –

  1. Invoke the RegisterForFeed() method and pass the groupName I’m willing to register and
  2. Subscribe to the “GetGroupFeed” action, through which group specific messages are relayed

I’ll add the below lines of code to the signalr.service.ts

private $groupFeed: Subject<Feed> = new Subject<Feed>();

public get GroupFeedObservable(): Observable<Feed> {
  return this.$groupFeed.asObservable();
}

public listenToGroupFeed() {
  (<HubConnection>this.hubConnection).on("GetGroupFeed", (data: Feed) => {
    console.log(data);
    this.$groupFeed.next(data);
  });
}

public joinGroupFeed(groupName: string) {
  return new Promise((resolve, reject) => {
    (<HubConnection>this.hubConnection)
    .invoke("RegisterForFeed", groupName)
    .then(() => {
      console.log("added to feed");
      return resolve(true);
    }, (err: any) => {
      console.log(err);
      return reject(err);
    });
  })
}

Finally, I’ll add a new component Group where I’ll subscribe to the messages relayed to a Group and bind them to the UI. The component receives the groupName from its route parameters and then subscribes to that group accordingly.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, Subscription } from 'rxjs';
import { Feed } from '../models/feed';
import { SignalrService } from '../services/signalr.service';

@Component({
  selector: 'app-group',
  templateUrl: './group.component.html',
  styleUrls: ['./group.component.css']
})
export class GroupFeedComponent implements OnInit, OnDestroy {
  $groupFeed: Observable<Feed>;
  groupFeed: Feed[] = [];
  $groupFeedSubject: Subscription | undefined;

  constructor(
    private route: ActivatedRoute, 
    private signalrService: SignalrService) {
    this.$groupFeed = this.signalrService.GroupFeedObservable;
  }

  ngOnInit(): void {
    this.route.paramMap.subscribe((map) => {
      let groupName: any = map.get('id');
      if (groupName) {
        this.signalrService.startConnection().then(() => {
          this.signalrService.joinGroupFeed(groupName).then(() => {
            this.signalrService.listenToGroupFeed();
            this.$groupFeedSubject = this.$groupFeed.subscribe((d: Feed) => {
              console.log(d);
              this.groupFeed.push(d);
            });
          }, (err) => {
            console.log(err);
          })
        })
      }
    });
  }

  ngOnDestroy(): void {
    this.$groupFeedSubject?.unsubscribe();
  }
}
<ng-container *ngIf="groupFeed.length > 0; else feedEmpty">
    <div *ngFor="let f of groupFeed" class="card rounded-0 my-2">
        <div class="card-header bg-transparent">
            <p class="card-text">
            {{ f.author }} posted in 
            <a [routerLink]="['/groups',f.groupName]">
            {{ f.groupName }}</a> on {{ f.created | date:'MM/dd/yyyy' }}</p>
        </div>
        <div class="card-body">
            <p class="card-text">{{ f.postText }}</p>
        </div>
    </div>
</ng-container>
<ng-template #feedEmpty>
    <p>No Feed yet.</p>
</ng-template>

I’ll then declare these components in AppModule and then add routes to these accordinly, which is a general approach.

When I run both the client and the server applications, the end result looks like this –

Conclusion

SignalR is a simple and efficient framework that makes building real-time web applications which rely on Pub-Sub models easy. It has a very good integration with popular languages and frameworks in the market and is built into the .NET Core library.

The above example tries to explain briefly about how we can build a realtime web communication, with a (near) realworld use case.

But there is still lot to cover about SignalR, such as one-to-one communication and in general about the fundamentals on which this framework works. So..

Check out the ebook!

I have published a mini-book on “Building Realtime Web Applications with SignalR and .NET 6”, where I covered all of the above concepts in detail along with some cool realworld examples such as Cab Tracking, Instant Messaging with full design and demonstration.

You can find the ebook here. Do check it out and let me know if 👇

How to Build Real time Web Applications with SignalR and .NET 6!

Buy Me A Coffee

Found this article helpful? Please consider supporting!

Share this with your friends and colleagues who might be interested! Leave a comment down below and let me know how you are going to implement it! ☺️


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

3 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *