The article shows how SignalR messages can be saved to a database using EF Core and SQLite. The post uses the SignalR Hub created in this blog; SignalR Group messages with ngrx and Angular, and extends it so that users can only join an existing SignalR group. The group history is then sent to the client that joined.
Code: https://github.com/damienbod/AspNetCoreAngularSignalR
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 Duende IdentityServer
- 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
Create the Database Store.
To create a store for the SignalR Hub, an EF Core Context is created and also the store logic which is responsible for accessing the database. The NewsContext class is really simple and just provides 2 DbSets, NewsItemEntities which will be used to save the SignalR messages, and NewsGroups which is used to validate and create the groups in the SignalR Hub.
using Microsoft.EntityFrameworkCore;
namespace AspNetCoreAngularSignalR.Providers;
public class NewsContext : DbContext
{
public NewsContext(DbContextOptions<NewsContext> options) :base(options){ }
public DbSet<NewsItemEntity> NewsItemEntities => Set<NewsItemEntity>();
public DbSet<NewsGroup> NewsGroups => Set<NewsGroup>();
}
The NewsStore provides the methods which will be used in the Hub and also an ASP.NET Core Controller to create, select the groups if required. The NewsStore uses the NewsContext class.
sing AspNetCoreAngularSignalR.SignalRHubs;
namespace AspNetCoreAngularSignalR.Providers;
public class NewsStore
{
public NewsStore(NewsContext newsContext)
{
_newsContext = newsContext;
}
private readonly NewsContext _newsContext;
public void AddGroup(string group)
{
_newsContext.NewsGroups.Add(new NewsGroup
{
Name = group
});
_newsContext.SaveChanges();
}
public bool GroupExists(string group)
{
var item = _newsContext.NewsGroups.FirstOrDefault(t => t.Name == group);
if(item == null)
{
return false;
}
return true;
}
public void CreateNewItem(NewsItem item)
{
if (GroupExists(item.NewsGroup))
{
_newsContext.NewsItemEntities.Add(new NewsItemEntity
{
Header = item.Header,
Author = item.Author,
NewsGroup = item.NewsGroup,
NewsText = item.NewsText
});
_newsContext.SaveChanges();
}
else
{
throw new System.Exception("group does not exist");
}
}
public List<NewsItem> GetAllNewsItems(string group)
{
return _newsContext.NewsItemEntities.Where(item => item.NewsGroup == group).Select(z =>
new NewsItem {
Author = z.Author,
Header = z.Header,
NewsGroup = z.NewsGroup,
NewsText = z.NewsText
}).ToList();
}
public List<string> GetAllGroups()
{
return _newsContext.NewsGroups.Select(t => t.Name ).ToList();
}
}
The NewsStore and the NewsContext are registered in the ConfigureServices method in the Startup class. The SignalR Hub is a singleton and so the NewsContext and the NewsStore classes are added as singletons. The AddDbContext requires the ServiceLifetime.Singleton parameter as this is not default. This is not optimal when using the NewsContext in the ASP.NET Core controller, as you need to consider the possible multiple client requests.
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.AddServerHeader = false;
});
var services = builder.Services;
var configuration = builder.Configuration;
var env = builder.Environment;
services.AddTransient<ValidateMimeMultipartContentFilter>();
var sqlConnectionString = configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<NewsContext>(options =>
options.UseSqlite(
sqlConnectionString
), ServiceLifetime.Singleton
);
services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigins",
builder =>
{
builder
.AllowCredentials()
.WithOrigins(
"https://localhost:4200")
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddSingleton<NewsStore>();
services.AddSignalR()
.AddMessagePackProtocol();
services.AddControllersWithViews();
Updating the SignalR Hub
The SignalR NewsHub uses the NewsStore which is injected using constructor injection. If a message is sent, or received, it is persisted using the CreateNewItem method from the store. When a new user joins an existing group, the history is sent to the client by invoking the “History” message.
using AspNetCoreAngularSignalR.Providers;
using Microsoft.AspNetCore.SignalR;
namespace AspNetCoreAngularSignalR.SignalRHubs;
public class NewsHub : Hub
{
private NewsStore _newsStore;
public NewsHub(NewsStore newsStore)
{
_newsStore = newsStore;
}
public Task Send(NewsItem newsItem)
{
if(!_newsStore.GroupExists(newsItem.NewsGroup))
{
throw new System.Exception("cannot send a news item to a group which does not exist.");
}
_newsStore.CreateNewItem(newsItem);
return Clients.Group(newsItem.NewsGroup).SendAsync("Send", newsItem);
}
public async Task JoinGroup(string groupName)
{
if (!_newsStore.GroupExists(groupName))
{
throw new System.Exception("cannot join a group which does not exist.");
}
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("JoinGroup", groupName);
var history = _newsStore.GetAllNewsItems(groupName);
await Clients.Client(Context.ConnectionId).SendAsync("History", history);
}
public async Task LeaveGroup(string groupName)
{
if (!_newsStore.GroupExists(groupName))
{
throw new System.Exception("cannot leave a group which does not exist.");
}
await Clients.Group(groupName).SendAsync("LeaveGroup", groupName);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
}
A NewsController is used to select all the existing groups, or add a new group, which is used by the SignalR Hub.
using Microsoft.AspNetCore.Mvc;
using AspNetCoreAngularSignalR.Providers;
namespace AspNetCoreAngularSignalR.Controllers;
[Route("api/[controller]")]
public class NewsController : Controller
{
private NewsStore _newsStore;
public NewsController(NewsStore newsStore)
{
_newsStore = newsStore;
}
[HttpPost]
public IActionResult AddGroup([FromQuery] string group)
{
if (string.IsNullOrEmpty(group))
{
return BadRequest();
}
_newsStore.AddGroup(group);
return Created("AddGroup", group);
}
public List<string> GetAllGroups()
{
return _newsStore.GetAllGroups();
}
}
Using the SignalR Hub
The NewsService Angular service, listens for SignalR events and handles these using the ngrx store.
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HubConnection } from '@microsoft/signalr';
import { NewsItem } from './models/news-item';
import { Store } from '@ngrx/store';
import * as newsAction from './store/news.action';
import * as signalR from '@microsoft/signalr';
import { Observable } from 'rxjs';
@Injectable()
export class NewsService {
private _hubConnection: HubConnection | undefined;
private actionUrl: string;
private headers: HttpHeaders;
constructor(private http: HttpClient, private store: Store<any>) {
this.init();
this.actionUrl = 'https://localhost:44324/api/news/';
this.headers = new HttpHeaders();
this.headers = this.headers.set('Content-Type', 'application/json');
this.headers = this.headers.set('Accept', 'application/json');
}
send(newsItem: NewsItem): NewsItem {
if (this._hubConnection) {
this._hubConnection.invoke('Send', newsItem);
}
return newsItem;
}
joinGroup(group: string): void {
if (this._hubConnection) {
this._hubConnection.invoke('JoinGroup', group);
}
}
leaveGroup(group: string): void {
if (this._hubConnection) {
this._hubConnection.invoke('LeaveGroup', group);
}
}
getAllGroups(): Observable<string[]> {
return this.http.get<string[]>(this.actionUrl, { headers: this.headers });
}
private init() {
this._hubConnection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:44324/looney')
.configureLogging(signalR.LogLevel.Information)
.build();
this._hubConnection.start().catch((err) => console.error(err.toString()));
this._hubConnection.on('Send', (newsItem: NewsItem) => {
this.store.dispatch(
newsAction.receiveNewsItemAction({ payload: newsItem })
);
});
this._hubConnection.on('JoinGroup', (data: string) => {
console.log('received data from the hub');
console.log(data);
this.store.dispatch(
newsAction.receiveGroupJoinedAction({ payload: data })
);
});
this._hubConnection.on('LeaveGroup', (data: string) => {
this.store.dispatch(newsAction.receiveGroupLeftAction({ payload: data }));
});
this._hubConnection.on('History', (newsItems: NewsItem[]) => {
console.log('received history from the hub');
console.log(newsItems);
this.store.dispatch(
newsAction.receiveNewsGroupHistoryAction({ payload: newsItems })
);
});
}
}
In the Angular application, when the user joins a group, he/she receives all the existing messages.
original pic: https://damienbod.files.wordpress.com/2017/09/signlargroups.gif

Links
https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction
https://github.com/aspnet/SignalR#readme
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
[…] Using EF Core and SQLite to persist SignalR Group messages in ASP.NET Core (Damien Bowden) […]
[…] Using EF Core and SQLite to persist SignalR Group messages in ASP.NET Core – Damien Bowden […]