Categories: Mobile App

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

This article should how 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

Other posts in this series:

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 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" Version="1.0.0-alpha2-final" />

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.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ApiServer.SignalRHubs
{
    [Authorize(AuthenticationSchemes = "Bearer")]
    public class UsersDmHub : Hub
    {
        private 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 }).InvokeAsync(
                   "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 }).InvokeAsync(
                    "NewOnlineUser",
                    _userInfoInMemory.GetUserInfo(Context.User.Identity.Name)
                    );
            }
            else
            {
                // existing user joined again
                
            }

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

            await Clients.Client(Context.ConnectionId).InvokeAsync(
                "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).InvokeAsync("SendDM", message, userInfoSender);
        }
    }
}

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

System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace ApiServer.SignalRHubs
{
    public class UserInfoInMemory
    {
        private ConcurrentDictionary<string, UserInfo> _onlineUser { get; set; } = new ConcurrentDictionary<string, UserInfo>();

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

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

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

            return userAlreadyExists;
        }

        public void Remove(string name)
        {
            UserInfo userInfo;
            _onlineUser.TryRemove(name, out userInfo);
        }

        public IEnumerable<UserInfo> GetAllUsersExceptThis(string username)
        {
            return _onlineUser.Values.Where(item => item.UserName != username);
        }

        public UserInfo GetUserInfo(string username)
        {
            UserInfo user;
            _onlineUser.TryGetValue(username, out user);
            return user;
        }
    }
}

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; }
        public string UserName { get; set; }
    }
}

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

var tokenValidationParameters = new TokenValidationParameters()
{
 ValidIssuer = "https://localhost:44318/",
 ValidAudience = "dataEventRecords",
 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("dataEventRecordsSecret")),
 NameClaimType = "name",
 RoleClaimType = "role", 
};

var jwtSecurityTokenHandler = new JwtSecurityTokenHandler
{
 InboundClaimTypeMap = new Dictionary<string, string>()
};

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
 options.Authority = "https://localhost:44318/";
 options.Audience = "dataEventRecords";
 options.IncludeErrorDetails = true;
 options.SaveToken = true;
 options.SecurityTokenValidators.Clear();
 options.SecurityTokenValidators.Add(jwtSecurityTokenHandler);
 options.TokenValidationParameters = tokenValidationParameters;
 options.Events = new JwtBearerEvents
 {
  OnMessageReceived = context =>
  {
   if ( (context.Request.Path.Value.StartsWith("/loo")) || (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 “@aspnet/signalr-client”: “1.0.0-alpha2-final”

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 'rxjs/add/operator/map';
import { Subscription } from 'rxjs/Subscription';

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

import { HubConnection } from '@aspnet/signalr-client';
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';

@Injectable()
export class DirectMessagesService {

    private _hubConnection: HubConnection;
    private headers: HttpHeaders;

    isAuthorizedSubscription: Subscription;
    isAuthorized: boolean;

    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 {

        this._hubConnection.invoke('SendDirectMessage', message, userId);
        return message;
    }

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

    join(): void {
        this._hubConnection.invoke('Join');
    }

    private init() {
        this.isAuthorizedSubscription = this.oidcSecurityService.getIsAuthorized().subscribe(
            (isAuthorized: boolean) => {
                this.isAuthorized = isAuthorized;
                if (this.isAuthorized) {
                    this.initHub();
                }
            });
        console.log('IsAuthorized:' + this.isAuthorized);
    }

    private initHub() {
        console.log('initHub');
        const token = this.oidcSecurityService.getToken();
        let tokenValue = '';
        if (token !== '') {
            tokenValue = '?token=' + token;
        }
        const url = 'https://localhost:44390/';
        this._hubConnection = new HubConnection(`${url}usersdm${tokenValue}`);

        this._hubConnection.on('NewOnlineUser', (onlineUser: OnlineUser) => {
            console.log('NewOnlineUser received');
            console.log(onlineUser);
            this.store.dispatch(new directMessagesActions.ReceivedNewOnlineUser(onlineUser));
        });

        this._hubConnection.on('OnlineUsers', (onlineUsers: OnlineUser[]) => {
            console.log('OnlineUsers received');
            console.log(onlineUsers);
            this.store.dispatch(new directMessagesActions.ReceivedOnlineUsers(onlineUsers));
        });

        this._hubConnection.on('Joined', (onlineUser: OnlineUser) => {
            console.log('Joined received');
            this.store.dispatch(new directMessagesActions.JoinSent());
            console.log(onlineUser);
        });

        this._hubConnection.on('SendDM', (message: string, onlineUser: OnlineUser) => {
            console.log('SendDM received');
            this.store.dispatch(new directMessagesActions.ReceivedDirectMessage(message, onlineUser));
        });

        this._hubConnection.on('UserLeft', (name: string) => {
            console.log('UserLeft received');
            this.store.dispatch(new directMessagesActions.ReceivedUserLeft(name));
        });

        this._hubConnection.start()
            .then(() => {
                console.log('Hub connection started')
                this._hubConnection.invoke('Join');
            })
            .catch(() => {
                console.log('Error while establishing connection')
            });
    }

}

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 { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { 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 { Observable } from 'rxjs/Observable';

@Component({
    selector: 'app-direct-message-component',
    templateUrl: './direct-message.component.html'
})

export class DirectMessagesComponent implements OnInit, OnDestroy {
    public async: any;
    onlineUsers: OnlineUser[];
    onlineUser: OnlineUser;
    directMessages: DirectMessage[];
    selectedOnlineUserName = '';
    dmState$: Observable<DirectMessagesState>;
    dmStateSubscription: Subscription;
    isAuthorizedSubscription: Subscription;
    isAuthorized: boolean;
    connected: boolean;
    message = '';

    constructor(
        private store: Store<any>,
        private oidcSecurityService: OidcSecurityService
    ) {
        this.dmState$ = this.store.select<DirectMessagesState>(state => state.dm.dm);
        this.dmStateSubscription = this.store.select<DirectMessagesState>(state => state.dm.dm)
            .subscribe((o: DirectMessagesState) => {
                this.connected = o.connected;
            });

    }

    public sendDm(): void {
        this.store.dispatch(new directMessagesAction.SendDirectMessageAction(this.message, this.onlineUser.userName));
    }

    ngOnInit() {
        this.isAuthorizedSubscription = this.oidcSecurityService.getIsAuthorized().subscribe(
            (isAuthorized: boolean) => {
                this.isAuthorized = isAuthorized;
                if (this.isAuthorized) {
                }
            });
        console.log('IsAuthorized:' + this.isAuthorized);
    }

    ngOnDestroy(): void {
        this.isAuthorizedSubscription.unsubscribe();
        this.dmStateSubscription.unsubscribe();
    }

    selectChat(onlineuserUserName: string): void {
        this.selectedOnlineUserName = onlineuserUserName
    }

    sendMessage() {
        console.log('send message to:' + this.selectedOnlineUserName + ':' + this.message);
        this.store.dispatch(new directMessagesAction.SendDirectMessageAction(this.message, this.selectedOnlineUserName));
    }

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

        return '';
    }

    disconnect() {
        this.store.dispatch(new directMessagesAction.Leave());
    }

    connect() {
        this.store.dispatch(new directMessagesAction.Join());
    }
}

The Angular HTML template displays the data using Angular material.

<div class="full-width" *ngIf="isAuthorized">
    <div class="left-navigation-container" >
        <nav>

            <mat-list>
                <mat-list-item *ngFor="let onlineuser of (dmState$|async)?.onlineUsers">
                    <a mat-button (click)="selectChat(onlineuser.userName)">{{onlineuser.userName}}</a>
                </mat-list-item>
            </mat-list>

        </nav>
    </div>
    <div class="column-container content-container">
        <div class="row-container info-bar">
            <h3 style="padding-left: 20px;">{{selectedOnlineUserName}}</h3>
            <a mat-button (click)="sendMessage()" *ngIf="connected && selectedOnlineUserName && selectedOnlineUserName !=='' && message !==''">SEND</a>
            <a mat-button (click)="disconnect()" *ngIf="connected">Disconnect</a>
            <a mat-button (click)="connect()" *ngIf="!connected">Connect</a>
        </div>

        <div class="content" *ngIf="selectedOnlineUserName && selectedOnlineUserName !==''">

            <mat-form-field  style="width:95%">
                <textarea matInput placeholder="your message" [(ngModel)]="message" matTextareaAutosize matAutosizeMinRows="2"
                          matAutosizeMaxRows="5"></textarea>
            </mat-form-field>
           
            <mat-chip-list class="mat-chip-list-stacked">
                <ng-container *ngFor="let directMessage of (dmState$|async)?.directMessages">

                    <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://github.com/aspnet/SignalR

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/msgpack5

Tags: , , , ,

This content was originally published here.

vinova

Recent Posts

Guide to Using AI in Recruitment Effectively in 2024

The recruitment picture is changing rapidly, and AI in recruitment is at the forefront of…

23 hours ago

What is Multimodal AI? 10 Creative Applications and Real-World Examples

Multimodal AI is a groundbreaking technology that combines multiple modalities, such as text, images, and…

2 days ago

Top 10 AI Applications in the Energy Sector for 2024

Artificial intelligence (AI in the energy) sector is revolutionizing how we produce, distribute, and consume…

3 days ago

Top Mobile App Monetization Strategies for 2024

Nowadays, monetization application is the end game of mobile app development. Whether you're an indie…

4 days ago

Top Reasons Why Mobile Apps Fail and Lessons for Success

Nowadays, many mobile apps fail to make an impact. From poor research to bad user…

5 days ago

Comprehensive Guide to VisionOS App Development 2024 for Beginners

Apple's VisionOS, the operating system powering the Vision Pro headset, is paving the way for…

6 days ago