Creating Real Time App with ASP.NET Core SignalR and React in Typescript

Vikas Sharma
The Startup
Published in
8 min readApr 5, 2020

SignalR now comes included with ASP.NET Core framework and they have made tons of improvement to make it lightweight and easier to use. To my surprise I couldn’t find any good tutorial on how to use SignalR, and use it to make something interesting other than the same old chat app. I thought then, let’s create something with SignalR other than the same boring chat app.

In this tutorial I will be guiding you through the main steps that are required to create a real time app. I will not write complete code here, you can find the complete source code on github.

The ScrumPoker App

In this tutorial we will be creating an interesting app called ScrumPoker. We live in Agile world so it’s very common that we do story estimation and pointing in our development or every sprint cycle. Back in days we used to have planning poker cards and team used to do story estimation via these cards but now everything is online, and we work remotely very often.

A user can create ScrumBoard and share the link with fellow teammates. Team members can enter their and start pointing the stories. Points given by the team will be shown on dashboard only when user who created ScrumBoard allows them to see.

Users gets added on the dashboard in the real time and points submitted by them also gets reflects on real time.

Create board
Dashboard
Real time user list update

Source Code

├───clientapp
├───Contracts
├───Controllers
├───Infrastructure
│ ├───NotificationHub
│ └───Persistence

You can download the complete source code from my github. Download it, clone it, fork it. https://github.com/vikas0sharma/ScrumPoker

Dev tools

We will be using ASP.NET Core 3.1, React 16.3+, Bootstrap 4, Node 10.13+, create-react-app, Redis, Visual Studio 2019, Visual Studio Code, Yarn package manager.

Here, I am assuming that you are familiar with ASP.Net Core environment and React. I would be guiding you the especial things you need to do to make SignalR work with React.

If you are new to SignalR I would suggest you have a look into the official documents of Microsoft.

And if you like React then definitely setting up the React development environment would be easy to you.

Basic Steps

  • Firstly, you need to create ASP.NET Core Web API project. Here you will be creating controller to handle requests from React app.
  • For our persistence we will be using Redis. Why Redis? Because I wanted to keep my app simple and besides that its an app that need its data to be persisted only when the app is running.
  • In ASP.Net core project folder you need to a create a separate folder for client app where all our React app code will reside.
  • I am using Yarn as package manager its your choice if you like NPM for your development.
  • I believe you are already familiar with create-react-app. It does all the heavy lifting for us and create a basic app structure. Point to notice here is that we will be writing our React app in Typescript. Why Typescript? Because, it makes developer’s life easy by catching silly bugs at development time.
yarn create react-app my-app --template typescript
  • You can use the package.json file from my source code that will help you in setting up all the required packages.

Backend Code

Let’s first setup our server side code. In our app, we are going to have only two models i.e. ScrumBoard and User.

Create Hub

SignalR via Hubs communicates between clients and servers. It’s the central location where we keep our communication logic. Here we specify who all clients will be notified.

using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;

namespace API.Infrastructure.NotificationHub
{
public class ScrumBoardHub : Hub
{
public async override Task OnConnectedAsync()
{
await base.OnConnectedAsync();
await Clients.Caller.SendAsync("Message", "Connected successfully!");
}

public async Task SubscribeToBoard(Guid boardId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, boardId.ToString());
await Clients.Caller.SendAsync("Message", "Added to board successfully!");
}

}
}

As you can see we have inherited from SignalR Hub class. On successful connection with client OnConnectedAsync will be called. Whenever a client connects to the hub a message will be pushed to the client.

We have exposed a method named ‘SubscribeToBoard’ that a client can call to subscribe to a scumboard by providing the scumboard id. If you notice we have used ‘Groups’ property of Hub to create a group of clients for a particular board. We will create a group by board id and add all the clients who have requested updates for that board.

On Dashboard, a user can see who others have joined in the board and what they are doing on dashboard in real time.

Register Hub in Startup

In ConfigureServices methods of Startup add AddSignalR.

services.AddSignalR();

In Configure method, register your Hub class.

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<ScrumBoardHub>("/scrumboardhub");// Register Hub class
});

Create Persistence

Like a said earlier, I am using Redis server to store tempory data/activities performed by user. Let’s create a class to perform CRUD operation using Redis. We will use StackExchange nuget package.

<PackageReference Include="StackExchange.Redis" Version="2.1.28" />

Setup Redis connection in Startup class.

services.Configure<APISettings>(Configuration);

services.AddSingleton<ConnectionMultiplexer>(sp =>
{
var settings = sp.GetRequiredService<IOptions<APISettings>>().Value;
var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);

configuration.ResolveDns = true;

return ConnectionMultiplexer.Connect(configuration);
});

Repository class:

using API.Contracts;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

namespace API.Infrastructure.Persistence
{
public class ScrumRepository : IScrumRepository
{
private readonly IDatabase database;

public ScrumRepository(ConnectionMultiplexer redis)
{
database = redis.GetDatabase();
}

public async Task<bool> AddBoard(ScrumBoard scrumBoard)
{
var isDone = await database.StringSetAsync(scrumBoard.Id.ToString(), JsonSerializer.Serialize(scrumBoard));

return isDone;
}

public async Task<bool> AddUserToBoard(Guid boardId, User user)
{
var data = await database.StringGetAsync(boardId.ToString());

if (data.IsNullOrEmpty)
{
return false;
}

var board = JsonSerializer.Deserialize<ScrumBoard>(data);
board.Users.Add(user);

return await AddBoard(board);
}

public async Task<bool> ClearUsersPoint(Guid boardId)
{
var data = await database.StringGetAsync(boardId.ToString());

if (data.IsNullOrEmpty)
{
return false;
}

var board = JsonSerializer.Deserialize<ScrumBoard>(data);
board.Users.ForEach(u => u.Point = 0);

return await AddBoard(board);
}

public async Task<List<User>> GetUsersFromBoard(Guid boardId)
{
var data = await database.StringGetAsync(boardId.ToString());

if (data.IsNullOrEmpty)
{
return new List<User>();
}

var board = JsonSerializer.Deserialize<ScrumBoard>(data);

return board.Users;
}

public async Task<bool> UpdateUserPoint(Guid boardId, Guid userId, int point)
{
var data = await database.StringGetAsync(boardId.ToString());
var board = JsonSerializer.Deserialize<ScrumBoard>(data);
var user = board.Users.FirstOrDefault(u => u.Id == userId);
if (user != null)
{
user.Point = point;
}

return await AddBoard(board);
}
}
}

A user can create a ScrumBoard where others user can create their profile and start voting or estimation of stories on the dashboard.

Let’s expose some endpoints for client app

We will create a controller class and expose some REST API that our React client app will use to send its request.

using API.Contracts;
using API.Infrastructure.NotificationHub;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace API.Controllers
{
[Route("scrum-poker")]
[ApiController]
public class ScrumPokerController : ControllerBase
{
private readonly IScrumRepository scrumRepository;
private readonly IHubContext<ScrumBoardHub> hub;

public ScrumPokerController(IScrumRepository scrumRepository, IHubContext<ScrumBoardHub> hub)
{
this.scrumRepository = scrumRepository;
this.hub = hub;
}

[HttpPost("boards")]
public async Task<IActionResult> Post([FromBody] ScrumBoard scrumBoard)
{
var boardId = Guid.NewGuid();
scrumBoard.Id = boardId;

var isCreated = await scrumRepository.AddBoard(scrumBoard);
if (isCreated)
{
return Ok(boardId);
}

return NotFound();
}

[HttpPost("boards/{boardId}")]
public async Task<IActionResult> UpdateUsersPoint(Guid boardId)
{
var isAdded = await scrumRepository.ClearUsersPoint(boardId);
await hub.Clients.Group(boardId.ToString())
.SendAsync("UsersAdded", await scrumRepository.GetUsersFromBoard(boardId));
if (isAdded)
{
return Ok(isAdded);
}
return NotFound();
}

[HttpPost("boards/{boardId}/users")]
public async Task<IActionResult> AddUser(Guid boardId, User user)
{
user.Id = Guid.NewGuid();
var isAdded = await scrumRepository.AddUserToBoard(boardId, user);
await hub.Clients.Group(boardId.ToString())
.SendAsync("UsersAdded", await scrumRepository.GetUsersFromBoard(boardId));
if (isAdded)
{
return Ok(user.Id);
}
return NotFound();
}

[HttpGet("boards/{boardId}/users")]
public async Task<IActionResult> GetUsers(Guid boardId)
{
var users = await scrumRepository.GetUsersFromBoard(boardId);

return Ok(users);
}

[HttpGet("boards/{boardId}/users/{userId}")]
public async Task<IActionResult> GetUser(Guid boardId, Guid userId)
{
var users = await scrumRepository.GetUsersFromBoard(boardId);
var user = users.FirstOrDefault(u => u.Id == userId);
return Ok(user);
}

[HttpPut("boards/{boardId}/users")]
public async Task<IActionResult> UpdateUser(Guid boardId, User user)
{
var isUpdated = await scrumRepository.UpdateUserPoint(boardId, user.Id, user.Point);
await hub.Clients.Group(boardId.ToString())
.SendAsync("UsersAdded", await scrumRepository.GetUsersFromBoard(boardId));

return Ok(isUpdated);
}
}
}

If you notice our controller is asking for IHubContext<ScrumBoardHub> in its constructor via dependency injection. This context class will be used to notify all the connected clients of a Group whenever user is added to the board or whenever a user submits his/her point or whenever admin clear outs points submitted by all users. SendAsync method sends the notification as well the updated list of users to the clients. Here message ‘UsersAdded’ may be misleading but it could be anything that you like, just keep in mind React app use this message to perform some action so make sure to keep in sync with React app.

Enable Cors

Request to start the SignalR connection gets blocked by CORS policy so we need to configure our ASP.NET to allow requests from React app as they will be hosted in different origins.

ConfigureServices method:

services.AddCors(options =>
options.AddPolicy("CorsPolicy",
builder =>
builder.AllowAnyMethod()
.AllowAnyHeader()
.WithOrigins("http://localhost:3000")
.AllowCredentials()));

Configure method:

app.UseCors("CorsPolicy");

Frontend Code

We will create separate components for board creation, user profile creation, dashboard, user list, header, navigation etc. But important point here is that we will keep our SignalR client logic in UserList components beacuse user’s list needs to be refreshed whenever some other user performs some activity.

Let’s write our SignalR connection code but before that we need to add SignalR package in our React app.

yarn add @microsoft/signalr

UserList component:

import React, { useState, useEffect, FC } from 'react';
import { User } from './user/User';
import { UserModel } from '../../models/user-model';
import { useParams } from 'react-router-dom';
import {
HubConnectionBuilder,
HubConnectionState,
HubConnection,
} from '@microsoft/signalr';
import { getBoardUsers } from '../../api/scrum-poker-api';

export const UserList: FC<{ state: boolean }> = ({ state }) => {
const [users, setUsers] = useState<UserModel[]>([]);
const { id } = useParams();
const boardId = id as string;
useEffect(() => {
if (users.length === 0) {
getUsers();
}
setUpSignalRConnection(boardId).then((con) => {
//connection = con;
});
}, []);

const getUsers = async () => {
const users = await getBoardUsers(boardId);
setUsers(users);
};

const setUpSignalRConnection = async (boardId: string) => {
const connection = new HubConnectionBuilder()
.withUrl('https://localhost:5001/scrumboardhub')
.withAutomaticReconnect()
.build();

connection.on('Message', (message: string) => {
console.log('Message', message);
});
connection.on('UsersAdded', (users: UserModel[]) => {
setUsers(users);
});

try {
await connection.start();
} catch (err) {
console.log(err);
}

if (connection.state === HubConnectionState.Connected) {
connection.invoke('SubscribeToBoard', boardId).catch((err: Error) => {
return console.error(err.toString());
});
}

return connection;
};
return (
<div className="container">
{users.map((u) => (
<User key={u.id} data={u} hiddenState={state}></User>
))}
</div>
);
};

we have created setUpSignalRConnection method which creates a connection using HubConnectionBuilder. It also listen to the ‘UserAdded’ message from the server and decides what to do what message+payload is arrived from server. It basically refreshes the user list with updated data sent by the server.

In our React app we have different components but they are pretty simple to understand that is why I am not mentioning them here.

Conclusion

Its very easy to setup SignalR with React and give our app the real time power. I have just mentioned the important steps required to setup SignalR. You can go through the complete source code to understand complete bits and pieces working together. Surely, there are improvements that we can do in our app like we can use Redux for communication between components.

With your support, I can keep creating and sharing my work with the world. I would appreciate you buying me a coffee.

--

--