Using EF Core and SQLite to persist SignalR Group messages in ASP.NET Core

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:

Creating 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 System;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace AspNetCoreAngularSignalR.Providers
{
    public class NewsContext : DbContext
    {
        public NewsContext(DbContextOptions<NewsContext> options) :base(options)
        { }

        public DbSet<NewsItemEntity> NewsItemEntities { get; set; }

        public DbSet<NewsGroup> NewsGroups { get; set; }
    }
}

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.

using AspNetCoreAngularSignalR.SignalRHubs;
using System;
using System.Collections.Generic;
using System.Linq;

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 IEnumerable<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
                });
        }

        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.

public void ConfigureServices(IServiceCollection services)
{
	var sqlConnectionString = Configuration.GetConnectionString("DefaultConnection");

	services.AddDbContext<NewsContext>(options =>
		options.UseSqlite(
			sqlConnectionString
		), ServiceLifetime.Singleton
	);

	services.AddCors(options =>
	{
		options.AddPolicy("AllowAllOrigins",
			builder =>
			{
				builder
					.AllowAnyOrigin()
					.AllowAnyHeader()
					.AllowAnyMethod();
			});
	});

	services.AddSingleton<NewsStore>();
	services.AddSignalR();
	services.AddMvc();
}

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;
using System.Threading.Tasks;

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).InvokeAsync("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.AddAsync(Context.ConnectionId, groupName);
            await Clients.Group(groupName).InvokeAsync("JoinGroup", groupName);

            var history = _newsStore.GetAllNewsItems(groupName);
            await Clients.Client(Context.ConnectionId).InvokeAsync("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).InvokeAsync("LeaveGroup", groupName);
            await Groups.RemoveAsync(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 AspNetCoreAngularSignalR.SignalRHubs;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
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 'rxjs/add/operator/map';

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import { HubConnection } from '@aspnet/signalr-client';
import { NewsItem } from './models/news-item';
import { Store } from '@ngrx/store';
import { NewsState } from './store/news.state';
import * as NewsActions from './store/news.action';

@Injectable()
export class NewsService {

    private _hubConnection: HubConnection;
    private actionUrl: string;
    private headers: HttpHeaders;

    constructor(private http: HttpClient,
        private store: Store<any>
    ) {
        this.init();
        this.actionUrl = 'http://localhost:5000/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 {
        this._hubConnection.invoke('Send', newsItem);
        return newsItem;
    }

    joinGroup(group: string): void {
        this._hubConnection.invoke('JoinGroup', group);
    }

    leaveGroup(group: string): void {
        this._hubConnection.invoke('LeaveGroup', group);
    }

    getAllGroups(): Observable<string[]> {
        return this.http.get<string[]>(this.actionUrl, { headers: this.headers });
    }

    private init() {

        this._hubConnection = new HubConnection('/looney');

        this._hubConnection.on('Send', (newsItem: NewsItem) => {
            this.store.dispatch(new NewsActions.ReceivedItemAction(newsItem));
        });

        this._hubConnection.on('JoinGroup', (data: string) => {
            this.store.dispatch(new NewsActions.ReceivedGroupJoinedAction(data));
        });

        this._hubConnection.on('LeaveGroup', (data: string) => {
            this.store.dispatch(new NewsActions.ReceivedGroupLeftAction(data));
        });

        this._hubConnection.on('History', (newsItems: NewsItem[]) => {
            this.store.dispatch(new NewsActions.ReceivedGroupHistoryAction(newsItems));
        });

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

}

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

Advertisements

2 comments

  1. […] Using EF Core and SQLite to persist SignalR Group messages in ASP.NET Core (Damien Bowden) […]

  2. […] Using EF Core and SQLite to persist SignalR Group messages in ASP.NET Core – Damien Bowden […]

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: