SignalR Notifications in .NET 7 and Angular

🤔What is SignalR?

SignalR is a real-time communication library for ASP.NET and .NET applications that enables bi-directional communication between clients (e.g., web browsers) and the server. Traditionally, web applications have been based on the request-response model, where a client sends a request to the server, and the server responds with the requested data. This approach works well for many scenarios but falls short when it comes to real-time interactions, such as chat applications, live updates, multiplayer games, stock tickers, and collaborative applications. By the end of this tutorial you will know how to build something like this.

🤔Why SignalR?

In the request-response model, clients need to poll the server periodically to check for updates, which can be inefficient and lead to unnecessary network traffic and server load. SignalR addresses this limitation by providing a persistent connection between the client and the server, allowing the server to push data to clients instantly when there are updates, rather than waiting for clients to request them.

SignalR supports various transport mechanisms, such as WebSocket, Server-Sent Events (SSE), Long Polling, and more, depending on the client's capabilities and the server's configuration. WebSocket is the most efficient transport mechanism for real-time communication, as it establishes a full-duplex connection between the client and the server, enabling low-latency communication and reducing overhead. If WebSocket is not available, SignalR gracefully falls back to other transport mechanisms to ensure broader compatibility.

Now, let's delve into the key concepts and components of SignalR:
  • Hubs: SignalR Hubs provide a high-level API for real-time communication. They abstract away the low-level details of different transport mechanisms and make it easier to broadcast messages to all connected clients or targeted groups of clients. Hubs are defined on the server and can be accessed from the client-side code.
  • Clients: Clients refer to the connected devices or applications that communicate with the server using SignalR. In web applications, clients are typically web browsers, but SignalR also supports other platforms like mobile apps and desktop applications.
  • Connection: A connection represents a persistent link between a client and the server. SignalR manages these connections transparently, and clients can send and receive messages over these connections.
  • Groups: Groups in SignalR allow you to organize clients into logical collections, making it easy to broadcast messages to specific subsets of clients. Clients can be dynamically added or removed from groups based on application logic.
Now, let's discuss how real-time notifications work with SignalR:

Imagine a scenario where you are developing a social media application, and you want to notify users instantly when they receive new messages or when someone likes their posts. With traditional request-response communication, the user's app would have to poll the server regularly to check for updates, which would be inefficient and lead to delays in delivering notifications.

With SignalR, you can implement real-time notifications as follows:
  • Set up SignalR: Install the SignalR package in your .NET application and configure it to use the desired transport mechanism (e.g., WebSocket).
  • Define SignalR Hub: Create a SignalR Hub on the server that handles the real-time notifications. This hub will have methods to send notifications to specific clients or groups.
  • Establish Connection: When a user logs in or opens the application, the client-side code establishes a connection to the SignalR Hub.
  • Subscribe to Events: Once the connection is established, the client can subscribe to specific events (e.g., "MessageReceived," "PostLiked") to receive notifications related to those events.
  • Server Broadcast: When a new message or a like action occurs, the server-side code invokes the relevant method on the SignalR Hub, passing the appropriate data as parameters.
  • Client Notification: The SignalR Hub receives the data and broadcasts it to all connected clients or specific groups. Clients that have subscribed to the relevant events will receive the notifications in real-time.
  • Handle Client Actions: When a client receives a notification, the client-side code can update the user interface, display a notification popup, or take any other action based on the nature of the notification.
By implementing real-time notifications using SignalR, you provide users with a more engaging and interactive experience, and you significantly reduce unnecessary network traffic and server load.

🧑🏻‍💻Let's Implement

Prerequisites

  • Basic dotnet and entity framework experience
  • Basic Angular experience
  • Code editor like VSCode
First clone this project to your computer so that its easier for you to follow along.


Open the `ClientApp` in VSCode, then start the client application with `ng serve`. Open `ServerApp.sln` with Visual Studio and run it to start the server application. 

For our application, we will be using alertifyjs to display notifications. We have added  `notification.service.ts` to display the notifications, and we're injecting it to the other components as needed.


Users can be registered with a role/group, log in and the add Todo items using this application. These operations are very basic and you should be able to understand what's happening by going through the code. What I would recommend is clone this repo which has the barebone boilerplate code, without the SignalR implementations.

git clone https://github.com/nishanc/AngularDotnetAuthDemo.git

If you run the application now you can register user, login using that user and add a todo item.

Our goal is not this, and to be honest you don't need all of these to implement SignalR, I went ahead and added these extra functionality so that we have a starting point.

Notifications

Our goal is to implement notifications, so let's start. First we need several packages to be installed in the client side and server side. [browse commit]

For the Angular application install @microsoft/signalr using

npm i @microsoft/signalr
Also for the server, we need Microsoft.AspNetCore.SignalR.Client. To install follow instructions given here.

Next create the NotificationHub and register it to the service container, so that we can inject it to our controllers/ services. [browse commit]

We have created NotificationHub and INotificationHub then in the Program.cs add following lines. Full Program.cs file can be found here.
 
builder.Services.AddSignalR(); // Add SignalR
builder.Services.AddCors(options => // Update CORS Policy
{
    options.AddPolicy(corsPolicy,
        b =>
        {
            b.WithOrigins(
                    "http://localhost:4200"
                )
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials()
                .SetIsOriginAllowed((hosts) => true);
        });
});
app.MapHub<NotificationHub>("/notification"); // Map SignalR Endpoint

Now we have a little bit of work to do. 

Client Side Implementation

Create a new Angular service named signalr. (ClientApp/src/app/services/signalr.service.ts)


This service has 3 functions. 
  1. startConnection() is responsible for initiating connection with the Hub that we created
  2. listenToNotifications() is responsible for listening to any incoming messages which has been sent by the server using "SendNotificationAsync" method.
  3. sendNotification() is responsible for calling "SendNotificationToUserAsync" method in the Hub. (that method then calls, SendNotificationAsync again. Look at the class NotificationHub)
So you have to understand that It's not only we can send messages to the client, also we can invoke methods in the server as well. After few more steps you can see what I mean.

Next we have to use these functions in our application. So in AppComponent (ClientApp/src/app/app.component.ts) add following lines.

constructor(private signalRService: SignalRService,
    private notificationService: NotificationService) {}
  ngOnInit(): void {
      this.signalRService.startConnection();
      this.signalRService.listenToNotifications((message) => {
        this.notificationService.message(message);
    });
}

Here when the application starts, we initiate the connection to the server and then start listening for the notifications. Because our listenToNotifications() accepts a callback, we're using that to call the notification service to display the notification when it's received.

Now in the TodoComponent (or wherever you want to invoke server methods to send notifications) inject the SignalRService.

constructor(private http: HttpClient,
    private notificationService: NotificationService,
    private signalRService: SignalRService) { }

Then let's call sendNotification() when a item is deleted.

deleteTodo(id: number) {
  this.http.delete(`${this.apiUrl}/todo/${id}`).subscribe({
    next: () => {
      this.todos = this.todos.filter(t => t.id !== id);
      this.signalRService.sendNotification("Item Deleted Notification from Client");
    },
    error: (e) => {
      this.notificationService.error(`Error occurred, check console`);
      console.error(e);
    }
  });
}

This will call the SendNotificationToUserAsync() method in our server.

Server Side Implementation

In the TodoController, inject the NotificationHub

private readonly ITodoRepository _repo;
private IHubContext<NotificationHub, INotificationHub> _notificationHub;

public TodoController(ITodoRepository repo, IHubContext<NotificationHub, INotificationHub> notificationHub)
{
    _repo = repo;
    _notificationHub = notificationHub;
}
Then use it to send notification when we add a Todo item, as follows.

[HttpPost]
public async Task<ActionResult> PostTodo(TodoDto todo)
{
    var newTodo = new Todo
    {
        Name = todo.Name
    };
    await _repo.AddTodo(newTodo); // Send notification when we add a Todo item
    await _notificationHub.Clients.All.SendNotificationAsync("Added Item Notification from Server");
    return Ok();
}

Now we're good to see this in action. Start the .NET server and Angular application, create two users using Register, one user with Administrator group and another in Normal User group.

Open http://localhost:4200/ in two browser windows and Login with those two users. The try to Add or Remove items.


You might have noticed that even though we delete the Item from one user, the UI is not updated in the other user. Let's fix that.

Update UI 

For this demo we will be using Angular EventEmitter. Create EventHandlerService with notificationEvent. [browse commit]

Then publish "refresh" event when there's a new notification.

ngOnInit(): void {
    this.signalRService.startConnection();
    this.signalRService.listenToNotifications((message) => {
      this.notificationService.message(message);
      this.eventHandlerService.notificationEvent.emit("refresh");
    });
}

Now subscribe to this event on TodoComponent, if the event is "refresh" then re-fetch the Todo items from the server.




Now the UI updates as expected. 

Send Notifications to a Group

So you might have noticed when we register a user we add them to a group, or you can say we're assigning a role to them. What if we want to send a notification only to the administrators? We can do that easily with SignalR. Let's modify few places. [browse commit]


We have added 2 new methods to the NotificationHubAddToGroupAsync: adds a connection to a specified group and SendNotificationToGroupAsync: invokes methods in a specified group. Now in the TodoController, or anywhere we want to send a notification only to the administrators only we can say,

await _notificationHub.Clients.Group(UserGroups.Administrator)
                .SendNotificationAsync("Added Item Notification from Server");

Now the PostTodo method should look like this.

Let's register users to a group now, we'll be doing this in the client side by adding a new function to the SignalRService as joinGroupFeed,

public joinGroupFeed(groupName: string) {
  return new Promise((resolve, reject) => {
    this.hubConnection
    .invoke("AddToGroupAsync", groupName)
    .then(() => {
      console.log("Added to group");
      return resolve(true);
    }, (err: any) => {
      console.log(err);
      return reject(err);
    });
  })
}

In the AuthController we need to add user role as a claim, so that we can decode that to get the user's role from the client side, then we can pass it to the method as groupName

In the Login method of AuthRepository change, (load UserGroup as well when we fetch the User from database)

var user = await _context.Users.FirstOrDefaultAsync(x => x.Username == username);
to

var user = await _context.Users.Include(x => x.UserGroup).FirstOrDefaultAsync(x => x.Username == username);Replace the text with codes

Then add the role to the claims in the Login method in AuthController.

new Claim(ClaimTypes.Role, userFromRepo.UserGroup.GroupName)

Now in the client side,  in the login.component.ts when user logs in we can assign the user to a group.

If you run the application now, "Added Item Notification from Server" notification should only be visible to the administrator users.


I'm gonna leave things there, it's up to your imagination now to think what you can build with the knowledge you got today. 👋🏻