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
- Getting started with SignalR using ASP.NET Core and Angular
- SignalR Group messages with ngrx and Angular
- Using EF Core and SQLite to persist SignalR Group messages in ASP.NET Core
- Securing an Angular SignalR client using JWT tokens with ASP.NET Core and IdentityServer4
- Implementing custom policies in ASP.NET Core using the HttpContext
- Sending Direct Messages using SignalR with ASP.NET core and Angular
- Using Message Pack with ASP.NET Core SignalR
- Uploading and sending image messages with ASP.NET Core SignalR
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://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
pretty cool stuff! thanks for the post 🙂
thanks
[…] Sending Direct Messages using SignalR with ASP.NET core and Angular – Damien Bowden […]
[…] Sending Direct Messages using SignalR with ASP.NET core and Angular (Damien Bowden) […]
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
The logic for connection management is not included or implemented. I don’t think this is stable yet with the alpha version.
Greetings Damien
[…] This content was originally published here. […]
Quite good and helpful example, do you have the same for the react?
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
The client code makes one weird thing: it assign hubConnection “on” handlers after starting the connection… This may some handler calls might be lost.
thanks, I need to fix this and also update this blog 🙂
Greetings Damien