Sending Direct Messages using SignalR with ASP.NET Core and Angular

This article shows how SignalR could be used to send direct messages between different clients using ASP.NET Core to host the SignalR Hub and Angular to implement the clients.

Code: https://github.com/damienbod/AspNetCoreAngularSignalRSecurity

Posts in this series

History

2023-01-08 Updated Angular 15, .NET 7
2021-01-25 Updated Angular 11.1.0 .NET 5, ngrx implementation
2020-03-21 updated packages, fixed Admin UI STS
2019-08-18 Updated ASP.NET Core 3.0, Angular 8.2.2
2019-02-06 Updated Angular 7.2.4, latest NGRX, SignalR CORS fix
2018-12-12 Updated .NET Core 2.2, ASP.NET Core SignalR 1.1.0, Angular 7.1.3
2018-05-31 Updated Microsoft.AspNetCore.SignalR 2.1
2018-05-08 Updated Microsoft.AspNetCore.SignalR 2.1 rc1, Angular 6
2018-03-15 Updated signalr Microsoft.AspNetCore.SignalR 1.0.0-preview1-final, Angular 5.2.8, @aspnet/signalr 1.0.0-preview1-update1

When the application is started, different clients can log in using an email, if already registered, and can send direct messages from one SignalR client to the other SignalR client using the email of the user which was used to sign in. All messages are sent using a JWT token which is used to validate the identity.

The latest Microsoft.AspNetCore.SignalR Nuget package can be added to the ASP.NET Core project in the csproj file, or by using the Visual Studio Nuget package manager to add the package.

 <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.1" />

A single SignalR Hub is used to add the logic to send the direct messages between the clients. The Hub is protected using the bearer token authentication scheme which is defined in the Authorize filter. A client can leave or join using the Context.User.Identity.Name, which is configured to use the email of the Identity. When the user joins, the connectionId is saved to the in-memory database, which can then be used to send the direct messages. All other online clients are sent a message, with the new user data. The actual client is sent the complete list of existing clients.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

namespace ApiServer.SignalRHubs;

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class UsersDmHub : Hub
{
    private readonly UserInfoInMemory _userInfoInMemory;

    public UsersDmHub(UserInfoInMemory userInfoInMemory)
    {
        _userInfoInMemory = userInfoInMemory;
    }

    public async Task Leave()
    {
        _userInfoInMemory.Remove(Context.User!.Identity!.Name);
        await Clients.AllExcept(new List<string> { Context.ConnectionId }).SendAsync(
               "UserLeft",
               Context.User.Identity.Name
               );
    }

    public async Task Join()
    {
        if (!_userInfoInMemory.AddUpdate(Context.User!.Identity!.Name, Context.ConnectionId))
        {
            // new user
            // var list = _userInfoInMemory.GetAllUsersExceptThis(Context.User.Identity.Name).ToList();
            await Clients.AllExcept(new List<string> { Context.ConnectionId }).SendAsync(
                "NewOnlineUser",
                _userInfoInMemory.GetUserInfo(Context.User.Identity.Name)
                );
        }
        else
        {
            // existing user joined again
            
        }

        await Clients.Client(Context.ConnectionId).SendAsync(
            "Joined",
            _userInfoInMemory.GetUserInfo(Context.User.Identity.Name)
            );

        await Clients.Client(Context.ConnectionId).SendAsync(
            "OnlineUsers",
            _userInfoInMemory.GetAllUsersExceptThis(Context.User!.Identity!.Name)
        );
    }

    public Task SendDirectMessage(string message, string targetUserName)
    {
        var userInfoSender = _userInfoInMemory.GetUserInfo(Context.User!.Identity!.Name);
        var userInfoReciever = _userInfoInMemory.GetUserInfo(targetUserName);
        return Clients.Client(userInfoReciever.ConnectionId).SendAsync("SendDM", message, userInfoSender);
    }
}

The UserInfoInMemory is used as an in-memory database, which is nothing more than a ConcurrentDictionary to manage the online users.

using System.Collections.Concurrent;

namespace ApiServer.SignalRHubs;

public class UserInfoInMemory
{
    private readonly ConcurrentDictionary<string, UserInfo> _onlineUser = new();

    public bool AddUpdate(string? name, string connectionId)
    {
        if (!string.IsNullOrEmpty(name))
        {
            var userAlreadyExists = _onlineUser.ContainsKey(name);

            var userInfo = new UserInfo
            {
                UserName = name,
                ConnectionId = connectionId
            };

            _onlineUser.AddOrUpdate(name, userInfo, (key, value) => userInfo);

            return userAlreadyExists;
        }

        throw new ArgumentNullException(nameof(name));
    }

    public void Remove(string? name)
    {
        if(!string.IsNullOrEmpty(name))
        {
            _onlineUser.TryRemove(name, out _);
        }
    }

    public IEnumerable<UserInfo> GetAllUsersExceptThis(string? username)
    {
        if(string.IsNullOrEmpty(username))
            return new List<UserInfo>();

        return _onlineUser.Values.Where(item => item.UserName != username);
    }

    public UserInfo GetUserInfo(string? username)
    {
        if (!string.IsNullOrEmpty(username))
        {
            _onlineUser.TryGetValue(username, out UserInfo? userInfo);
            if(userInfo != null)
                return userInfo;
        }

        throw new ArgumentNullException(nameof(username));
    }
}

The UserInfo class is used to save the ConnectionId from the SignalR Hub, and the user name.

namespace ApiServer.SignalRHubs;

public class UserInfo
{
    public string ConnectionId { get; set; } = string.Empty;
    public string UserName { get; set; } = string.Empty;
}

The JWT Bearer token is configured in the startup class, to read the token from the URL parameters.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
	.AddJwtBearer(options =>
	{
		options.Authority = "https://localhost:5001/";
		options.Audience = "dataEventRecordsApi";
		options.IncludeErrorDetails = true;
		options.SaveToken = true;
		options.TokenValidationParameters = new TokenValidationParameters
		{
			ValidateIssuer = true,
			ValidateAudience = true,
			ValidateIssuerSigningKey = true,
			NameClaimType = "email",
			RoleClaimType = "role",
			ValidAudiences = new List<string> { "dataEventRecordsApi" },
			ValidIssuers = new List<string> { "https://localhost:5001/" }
		};
		options.Events = new JwtBearerEvents
		{
			OnMessageReceived = context =>
			{
				if ( ( context.Request.Path.Value!.StartsWith("/signalrhome")
					|| context.Request.Path.Value!.StartsWith("/looney")
					|| context.Request.Path.Value!.StartsWith("/usersdm") 
				   )
					&& context.Request.Query.TryGetValue("token", out StringValues token)
				)
				{
					context.Token = token;
				}

				return Task.CompletedTask;
			},
			OnAuthenticationFailed = context =>
			{
				var te = context.Exception;
				return Task.CompletedTask;
			}
		};
});

Angular SignalR Client

The Angular SignalR client is implemented using the npm package “@microsoft/signalr”: “7.0.0”

A ngrx store is used to manage the states sent, received from the API. All SiganlR messages are sent using the DirectMessagesService Angular service. This service is called from the ngrx effects, or sends the received information to the reducer of the ngrx store.

import { Subscription, Observable } from 'rxjs';

import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';

import { HubConnection } from '@microsoft/signalr';
import { Store } from '@ngrx/store';
import * as directMessagesActions from './store/directmessages.action';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { OnlineUser } from './models/online-user';
import * as signalR from '@microsoft/signalr';

@Injectable()
export class DirectMessagesService {
  private hubConnection: HubConnection | undefined;
  private headers: HttpHeaders | undefined;
  private token: string = '';

  isAuthorizedSubscription: Subscription | undefined;
  isAuthorized = false;

  constructor(
    private store: Store<any>,
    private oidcSecurityService: OidcSecurityService
  ) {
    this.headers = new HttpHeaders();
    this.headers = this.headers.set('Content-Type', 'application/json');
    this.headers = this.headers.set('Accept', 'application/json');

    this.init();
  }

  sendDirectMessage(message: string, userId: string): string {
    if (this.hubConnection) {
      this.hubConnection.invoke('SendDirectMessage', message, userId);
    }
    return message;
  }

  leave(): void {
    if (this.hubConnection) {
      this.hubConnection.invoke('Leave');
    }
  }

  join(): void {
    console.log('DMS: send join');
    if (this.hubConnection) {
      this.hubConnection.invoke('Join');
    }
  }

  private init(): void {

    this.oidcSecurityService.isAuthenticated$.subscribe(({ isAuthenticated }) => {
      this.isAuthorized = isAuthenticated;

      if (this.isAuthorized) {
        this.initHub();
      }

      console.warn('authenticated: ', isAuthenticated);
    });
  }

  private initHub(): void {
    console.log('DMS: initHub');

    this.oidcSecurityService.getAccessToken().subscribe((token) => {

      let tokenValue = '';
      this.token = token;
      const tokenApiHeader = 'Bearer ' + this.token;
      this.headers = this.headers.append('Authorization', tokenApiHeader);
      console.log(tokenApiHeader)
      tokenValue = '?token=' + token;


      const url = 'https://localhost:44390/';

      this.hubConnection = new signalR.HubConnectionBuilder()
        .withUrl(`${url}usersdm${tokenValue}`)
        .configureLogging(signalR.LogLevel.Information)
        .build();

      this.hubConnection.start().catch((err) => console.error(err.toString()));

      this.hubConnection.on('NewOnlineUser', (onlineUser: OnlineUser) => {
        console.log('DMS: NewOnlineUser received');
        console.log(onlineUser);
        this.store.dispatch(
          directMessagesActions.receivedNewOnlineUserAction({
            payload: onlineUser,
          })
        );
      });

      this.hubConnection.on('OnlineUsers', (onlineUsers: OnlineUser[]) => {
        console.log('DMS: OnlineUsers received');
        console.log(onlineUsers);
        this.store.dispatch(
          directMessagesActions.receivedOnlineUsersAction({
            payload: onlineUsers,
          })
        );
      });

      this.hubConnection.on('Joined', (onlineUser: OnlineUser) => {
        console.log('DMS: Joined received');
        console.log(onlineUser);
      });

      this.hubConnection.on(
        'SendDM',
        (message: string, onlineUser: OnlineUser) => {
          console.log('DMS: SendDM received');
          this.store.dispatch(
            directMessagesActions.receivedDirectMessageForUserAction({
              payload: { onlineUser, message },
            })
          );
        }
      );

      this.hubConnection.on('UserLeft', (name: string) => {
        console.log('DMS: UserLeft received');
        this.store.dispatch(
          directMessagesActions.receivedUserLeftAction({ payload: name })
        );
      });
    });
  }
}

The DirectMessagesComponent is used to display the data, or send the events to the ngrx store, which in turn, sends the data to the SignalR server.

import { sendDirectMessageAction } from './../store/directmessages.action';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { DirectMessagesState } from '../store/directmessages.state';
import * as directMessagesAction from '../store/directmessages.action';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { OnlineUser } from '../models/online-user';
import { DirectMessage } from '../models/direct-message';
import * as fromSelectorsStore from '../store/directmessages.selectors';

@Component({
  selector: 'app-direct-message-component',
  templateUrl: './direct-message.component.html',
})
export class DirectMessagesComponent implements OnInit {
  public async: any;
  onlineUser: OnlineUser = { connectionId: '', userName: '' };
  selectedOnlineUserName = '';
  isAuthorized = false;
  message = '';
  onlineUsers$: Observable<OnlineUser[]>;
  directMessages$: Observable<DirectMessage[]>;
  // connected$: Observable<boolean>;
  connected = false;
  constructor(
    private store: Store<any>,
    private oidcSecurityService: OidcSecurityService
  ) {
    this.onlineUsers$ = this.store.pipe(
      select(fromSelectorsStore.selectOnlineUsers)
    );
    this.directMessages$ = this.store.pipe(
      select(fromSelectorsStore.selectDirectMessages)
    );

    this.store
      .pipe(select(fromSelectorsStore.selectConnected))
      .subscribe((data) => {
        console.log(data);
        this.connected = data;
      });
  }

  ngOnInit(): void {

    this.oidcSecurityService.isAuthenticated$.subscribe(({ isAuthenticated }) => {
      this.isAuthorized = isAuthenticated;

      console.warn('authenticated: ', isAuthenticated);
    });

    console.log('IsAuthorized:' + this.isAuthorized);
  }

  selectChat(onlineuserUserName: string): void {
    console.log('DMC: selectedOnlineUserName' + onlineuserUserName);
    this.selectedOnlineUserName = onlineuserUserName;
  }

  sendMessage(): void {
    console.log(
      'DMC: send message to:' + this.selectedOnlineUserName + ':' + this.message
    );

    const message = {
      payload: {
        message: this.message,
        userNameTarget: this.selectedOnlineUserName,
      },
    };

    this.store.dispatch(directMessagesAction.sendDirectMessageAction(message));
  }

  getUserInfoName(directMessage: DirectMessage): string {
    if (directMessage.fromOnlineUser) {
      return directMessage.fromOnlineUser.userName;
    }

    return '';
  }

  disconnect(): void {
    this.store.dispatch(directMessagesAction.leaveAction());
  }

  connect(): void {
    this.store.dispatch(directMessagesAction.joinAction());
  }
}

The Angular HTML template displays the data using Angular material.

<div class="full-width" *ngIf="isAuthorized">
  <div class="left-navigation-container">
    <nav *ngIf="connected">
      <mat-list>
        <mat-list-item *ngFor="let onlineuser of onlineUsers$ | async">
          <a mat-button (click)="selectChat(onlineuser.userName)">{{
            onlineuser.userName
          }}</a>
        </mat-list-item>
      </mat-list>
    </nav>
  </div>
  <div class="column-container content-container">
    <div>
      <div class="row-container info-bar">
        <h3 style="padding-left: 20px">{{ selectedOnlineUserName }}</h3>
        <a
          mat-button
          (click)="sendMessage()"
          *ngIf="connected && selectedOnlineUserName && message !== ''"
          >SEND</a
        >
        <a mat-button (click)="disconnect()" *ngIf="connected">Disconnect</a>
        <a mat-button (click)="connect()" *ngIf="!connected">Connect</a>
      </div>
    </div>

    <div class="content" *ngIf="selectedOnlineUserName">
      <mat-form-field style="width: 95%">
        <textarea
          matInput
          placeholder="your message"
          [(ngModel)]="message"
          cdkTextareaAutosize
          cdkAutosizeMinRows="2"
          cdkAutosizeMaxRows="5"
        ></textarea>
      </mat-form-field>

      <mat-chip-list class="mat-chip-list-stacked">
        <ng-container *ngFor="let directMessage of directMessages$ | async">
          <ng-container *ngIf="getUserInfoName(directMessage) !== ''">
            <mat-chip selected="true" style="width: 95%">
              {{ getUserInfoName(directMessage) }} {{ directMessage.message }}
            </mat-chip>
          </ng-container>

          <ng-container *ngIf="getUserInfoName(directMessage) === ''">
            <mat-chip style="width: 95%">
              {{ getUserInfoName(directMessage) }} {{ directMessage.message }}
            </mat-chip>
          </ng-container>
        </ng-container>
      </mat-chip-list>
    </div>
  </div>
</div>

Links

https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction

https://github.com/aspnet/SignalR#readme

https://github.com/ngrx

https://www.npmjs.com/package/@aspnet/signalr-client

https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json

https://dotnet.myget.org/F/aspnetcore-ci-dev/npm/

https://dotnet.myget.org/feed/aspnetcore-ci-dev/package/npm/@aspnet/signalr-client

https://www.npmjs.com/package/msgpack

11 comments

  1. pretty cool stuff! thanks for the post 🙂

  2. […] Sending Direct Messages using SignalR with ASP.NET core and Angular – Damien Bowden […]

  3. […] Sending Direct Messages using SignalR with ASP.NET core and Angular (Damien Bowden) […]

  4. How are you dealing with losing connection? I am using angular 5 and signalr closes connection after 100 seconds and I have to reconnect manually, but events that have been emitted during that time don’t come

    1. The logic for connection management is not included or implemented. I don’t think this is stable yet with the alpha version.

      Greetings Damien

  5. Farid Huseynov · · Reply

    Quite good and helpful example, do you have the same for the react?

  6. when clicking “Angular app” says connection is refused. project runs ok but when clicking angular app doesnt evern show something, just connection is refused and shows another port

  7. The client code makes one weird thing: it assign hubConnection “on” handlers after starting the connection… This may some handler calls might be lost.

    1. thanks, I need to fix this and also update this blog 🙂

      Greetings Damien

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: