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 of creating something with SignalR other than the same boring chat app.
Introduction
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 the 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 an Agile world so it’s very common that we do story estimation and pointing in our development or every sprint cycle. Back in the 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 there and start pointing out the stories. Points given by the team will be shown on the dashboard only when user who created ScrumBoard
allows them to see.
Users get added on the dashboard in real-time and points submitted by them also get reflected in real-time.
Source Code
├───clientapp
├───Contracts
├───Controllers
├───Infrastructure
│ ├───NotificationHub
│ └───Persistence
You can download the complete source code from my github. Download it, clone it, fork it from 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 with the special 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 for you.
Basic Steps
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 which 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 else has joined on 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");
});
Create Persistence
Like I said earlier, I am using Redis server to store temporary 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 a 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 to 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 the important point here is that we will keep our SignalR client logic in UserList
components because 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) => {
});
}, []);
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 listens to the ‘UserAdded
’ message from the server and decides what to do what message+payload arrives from the 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
It's 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 make in our app like we can use Redux for communication between components.
History
- 7th April, 2020: Article published