.Net WebApi - SignalR & Angular


Source code
Code available at GitHub
About SignalR
Full documentation available here
Fundamental concepts
SignalR acts as an abstraction layer over remote procedure calls (RPC), streamlining their use. It primarily employs the WebSocket network protocol, but can fallback to other transport protocols like HTML5 or Comet when necessary. A key concept in SignalR is the Hub, which facilitates communication between clients and the server, allowing clients to invoke server methods and vice versa. Hubs are transient, meaning they should not store state. To call methods outside a hub, IHubContext is used. SignalR supports strongly typed hubs, reducing the risk of runtime errors - there is no need to use strings for method names. Hubs can target all connected clients, specific connections, groups, or individual users. Additionally, SignalR integrates with ASP.NET Core authentication, associating users with each connection. Authentication data is accessible within the hub via HubConnectionContext.User, similar to WebApi.
Supported platforms
Server side:
- any server platform that ASP.NET Core supports
Client side:
JavaScript/TypeScript
.NET runs on ASP.NET Core
Java
Swift
Use cases from sample project
Retrieving status of executed tasks with single client
Retrieving status of executed tasks with two clients plus run request that returns result only to caller
Client executes method on server side to get number of running tasks
Requirements
.NET
Install SignalR NuGet package
Microsoft.AspNetCore.SignalR
Angular
Install SignalR npm package
npm install @microsoft/signalr
Configuration
This section describes the application setup details related specifically to SignalR - surprisingly there is not much to do in scope of configuring the app. Things like CORS, HTTPS redirection, etc. are not covered here. For sake of the sample project simplicity the authorization concept was ignored.
.NET
Program.cs
// ...
// Add SignalR services
builder.Services.AddSignalR();
// ...
// Map incoming requests with a specific hub
app.MapHub<TasksHub>("/tasksHub");
app.Run();
// ...
Core parts of the sample project
.NET side
webapi/Api/TasksApi.Core/TasksHub.cs
Contract for strongly typed Hub
public interface ITasksStatusClient
{
// Called on client-side by server
Task TasksStatuses(IReadOnlyCollection<TaskDto> tasks);
// Called on client-side by server
Task TaskStatus(TaskDto task);
// Called on server-side by clients
Task<int> RunningTasksCount(int tasksCount);
}
Hub implementation
public class TasksHub : Hub<ITasksStatusClient>
{
private readonly ITasksStatusService _taskStatusService;
// Task service injected into the Hub to fetch data from in-memory storage
public TasksHub(ITasksStatusService taskStatusService)
{
_taskStatusService = taskStatusService ?? throw new ArgumentNullException(nameof(taskStatusService));
}
// This method can be called by clients
public async Task<int> RunningTasksCount()
{
return await Task.FromResult(_taskStatusService.RunningTasksCount);
}
}
webapi/Api/TasksApi.Core/TasksStatusService.cs
Example of calling the method on client side
// Call method for all clients
public Task ExecuteTask(TaskDto task)
{
return ExecuteTaskInternal(task, (t) =>
_tasksStatusHubContext.Clients.All.TaskStatus(t));
}
// Call method only for specific connection (caller)
public Task ExecuteTask(TaskDto task, string connectionId)
{
return ExecuteTaskInternal(task, (t) =>
_tasksStatusHubContext.Clients.Client(connectionId).TaskStatus(t));
}
// Call method for all clients
public async Task RemoveTask(int id)
{
_taskStorage.Delete(id);
await _tasksStatusHubContext.Clients.All.TasksStatuses(_taskStorage.GetAll());
}
Angular side
angular/src/app/services/signalR/tasks-signalr.service.ts
Build hub connection
constructor() {
this.hubConnection = new HubConnectionBuilder()
.withUrl(environment.signalRBaseUrl + "tasksHub")
.build();
}
Establish connection with hub, executed in root component under OnInit method.
public start() {
if (this.hubConnection == null) {
return;
}
// Start Hub connection
this.hubConnection
.start()
.then(() => {
console.log('Connected to tasks hub.');
// We can store connection ID to make further calls with response only for caller
this.connectionId = this.hubConnection.connectionId ?? '';
})
.catch(error => console.log('Tasks hub connection error: ' + error));
// Register hub method that can be called by server on client side
this.hubConnection.on('TasksStatuses', (tasks: TaskDto[]) => {
this.tasksStatusesSubject.next(tasks);
})
// Register hub method that can be called by server on client side
this.hubConnection.on('TaskStatus', (task: TaskDto) => {
this.taskStatusSubject.next(task);
});
}
Close connection to hun, executed in root component under OnDestroy method
public stop() {
if (this.hubConnection == null) {
return;
}
this.hubConnection.stop().then(() => {
console.log('Disconnected from tasks hub.');
});
}
Call method on service-side
public getRunningTasksCount(): Observable<number> {
return new Observable<number>((subscriber) => {
this.hubConnection
// Provide expected return type and method name
// See webapi/Api/TasksApi.Core/TasksHub.cs for Hub contract
.invoke<number>('RunningTasksCount')
.then((count) => {
subscriber.next(count);
subscriber.complete();
})
.catch((err) => {
subscriber.error(err);
});
});
}
Subscribe to my newsletter
Read articles from Mateusz Czernek directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
