.Net WebApi - SignalR & Angular

Mateusz CzernekMateusz Czernek
4 min read

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);
        });
    });
  }
0
Subscribe to my newsletter

Read articles from Mateusz Czernek directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mateusz Czernek
Mateusz Czernek