How to use SignalR to build a real-time communication application communicating with Angular in ASP.NET Core

picture

Suppose we want to create a monitoring Web application that provides users with a dashboard that can display a series of information that will be updated over time.

The first method is to call the API periodically at a defined time interval (polling) to update the data on the dashboard.

Anyway, there is still a problem: if there is no updated data, we will unnecessarily increase network traffic due to requests.

An alternative is long polling: if the server has no data available, it can keep the request active until something happens or a preset timeout is reached, rather than sending an empty response. If there is new data, the complete response will reach the client. A completely different approach is to reverse the role: when new data is available (pushed), the back end contacts the client.

Remember that HTML 5 has a standardized WebSocket, which is a permanent two-way connection that can be configured using a Javascript interface in a compatible browser. Unfortunately, WebSocket must be fully supported on both the client and server sides to make it available. Then, we need to provide an alternative system (fallback), which allows our application to run anyway.

Microsoft released an open source library called SignalR for ASP.NET in 2013, which has been rewritten for ASP.NET Core in 2018. SignalR abstracts from all the details related to the communication mechanism and selects the best one from the available information.

As a result, it is possible to write code, just as we have been in push mode. With SignalR, the server can call JavaScript methods on all its connected clients or on specific clients.

We use the web API template to create an ASP.NET Core project and delete the generated sample controller. Using NuGet, we added Microsoft.AspNet.SignalR to the project to create a Hub.

A hub is a high-level pipeline that can call client code and send a message containing the name and parameters of the requested method. Objects sent as parameters are deserialized using the appropriate protocol. The client searches the page code for the method corresponding to the name. If the name is found, it will call it and pass the deserialized data as a parameter.

using Microsoft.AspNetCore.SignalR;

namespace SignalR.Hubs
{
    public class NotificationHub : Hub { }
}

As you may know, in the ASP.NET Core, you can configure the management pipeline of HTTP requests to add some middleware, which can intercept requests, add configured functions and move them to the next middleware. The SignalR middleware must be pre configured in the ConfigureServices of Startup class

Add the extension method services.AddSignalR() to the method. Now we can use the Startup class

The extension method app.UseSignalR () in the Configure method adds the middleware to the pipeline.

During this operation, we can pass configuration parameters, including hub Routing:

app.UseSignalR(route =>
{
    route.MapHub<notificationhub>("/notificationHub");
})

An interesting scenario allows us to view another interesting feature in the ASP.NET Core, that is, hosting the SignalR Hub in the context of the background worker process. Suppose we want to implement the following use cases:

• running business logic

• wait a minute • decide whether to stop or repeat the process.

In ASP.NET Core, we can use the IHostedService interface provided by the framework to implement the process execution in the background in. NET Core applications. The methods to be implemented are StartAsync () and StopAsync (). It's very simple: StartAsync calls to the host to start, and StopAsync calls to the host to shut down.

Then, we add a class DashboardHostedService to the project, which implements

IHostedService. We add interface registration in the ConfigureServices method of Startup class:

services.AddHostedService<dashboardhostedservice>();

In the class constructor DashboardHostedService, we inject IHubContext

Access the hub added to our application. In the method StartAsync, we set a timer that will run the code contained in the method DoWork () every two seconds. This method sends a message with four randomly generated strings.

But to whom does it spread? In our example, we are sending messages to all connected clients. However, SignalR provides an opportunity to send messages to a single user or group of users. In this article [1], you will find detailed information about authentication and authorization functions in ASP.NET Core.

Interestingly, users can connect on both desktop and mobile devices. Each device has a separate SignalR connection, but they will all be associated with the same user.

using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using SignalR.Hubs;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SignalR
{
    public class DashboardHostedService: IHostedService
    {
        private Timer _timer;
        private readonly IHubContext<notificationhub> _hubContext;

        public DashboardHostedService(IHubContext<notificationhub> hubContext)
        {
            _hubContext = hubContext;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(2));

            return Task.CompletedTask;
        }

        private void DoWork(object state)
        {
            _hubContext.Clients.All.SendAsync("SendMessage", 
                new {
                    val1 = getRandomString(),
                    val2 = getRandomString(),
                    val3 = getRandomString(),
                    val4 = getRandomString()
                });
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _timer?.Change(Timeout.Infinite, 0);

            return Task.CompletedTask;
        }
    }
}

Let's look at how to manage the client part. For example, we use the ng new SignalR command of the Angular CLI to create an Angular application.

Then we install the package node of SignalR(

npm i @ aspnet / signalr

). Then add a service that allows us to connect to the hub we created earlier and receive messages. Here, the first possible method is to use the private declared Subject to return based on the Observable service in the service getMessage() (Message is the Typescript interface corresponding to the Object returned from the Object. Back end):

@Injectable({
 providedIn: 'root'
})
export class SignalRService {
 private message$: Subject<message>;
 private connection: signalR.HubConnection;

 constructor() {
   this.message$ = new Subject<message>();
   this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubUrl)
   .build();
   this.connect();
 }
 private connect() {
   this.connection.start().catch(err => console.log(err));
   this.connection.on('SendMessage', (message) => {
     this.message$.next(message);
   });
 }
 public getMessage(): Observable<message> {
   return this.message$.asObservable();
 }
 public disconnect() {
   this.connection.stop();
 }
}

Inside the constructor (), we create an object of type SignalR.HubConnection, which will be used to connect to the server. We pass it to the central URL by using the file environment.ts:

this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubUrl)
   .build();

The constructor is also responsible for calling the connect () method, which makes the actual connection and logs possible errors in the console.

this.connection.start().catch(err => console.log(err));
this.connection.on('SendMessage', (message) => {
  this.message$.next(message);
});

Components that want to display messages from the back end (services that inject them into the constructor) should subscribe to the getMessage () method and manage incoming messages. Take AppComponent as an example, for example:

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
 private signalRSubscription: Subscription;

 public content: Message;

 constructor(private signalrService: SignalRService) {
   this.signalRSubscription = this.signalrService.getMessage().subscribe(
     (message) => {
       this.content = message;
   });
 }
 ngOnDestroy(): void {
   this.signalrService.disconnect();
   this.signalRSubscription.unsubscribe();
 }
}

Using topics allows us to manage more components at the same time, regardless of the messages returned from the center (for subscription or unsubscribe), but we must pay attention to the careless use of topics. Let's consider the following getMessage () versions:

public getMessage(): Observable<message> {
   return this.message$;
}

Now, the component can also send a message using the following simple code:

const produceMessage = this.signalrService.getMessage() as Subject<any>;
 produceMessage.next( {val1: 'a'});
</any>

If the method getMessage() returns SubjectasObservable, this code will throw an exception! The second (simpler) approach we can use in the case of a single component is interested in managing messages from the back end:

@Injectable({
 providedIn: 'root'
})
export class SignalrService {
 connection: signalR.HubConnection;

 constructor() {
   this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubAddress)
   .build();
   this.connect();
 }

 public connect() {
   if (this.connection.state === signalR.HubConnectionState.Disconnected) {
     this.connection.start().catch(err => console.log(err));
   }
 }

 public getMessage(next) {
     this.connection.on('SendMessage', (message) => {
       next(message);
     });
 }

 public disconnect() {
   this.connection.stop();
 }
}

We can simply pass the function callback to the method getMessage, which takes the message from the back end as a parameter. In this case, AppComponent can be:

public content: IMessage;
constructor(private signalrService: SignalrService) {
   this.signalrService.getMessage(
     (message: IMessage) => {
       this.content = message;
     }
   );
}
ngOnDestroy(): void {
   this.signalrService.disconnect();
}

The last few lines of code are located in app.component.html and app.component.css, respectively

, to give some fashion, and the application has been completed.

<div style="text-align:center">
  <h1>
    DASHBOARD
  </h1>
</div>
<div class="card-container">
  <div class="card">
    <div class="container">
      <h4><b>Valore 1</b></h4>
      <p>{{content.val1}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 2</b></h4>
      <p>{{content.val2}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 3</b></h4>
      <p>{{content.val3}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 4</b></h4>
      <p>{{content.val4}}</p>
    </div>
  </div>
</div>

.card-container {
  display: flex;
  flex-wrap: wrap;
}

.card {
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  transition: 0.3s;
  width: 40%;
  flex-grow: 1;
  margin: 10px;
}

.card:hover {
  box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}

.container {
  padding: 2px 16px;
}

We start the back end first, then start the front end and check the final result:

It seems all right! You can find the code here: HTTPS [2]: / / GitHub. COM / aarnold87 / signalrwithangular [3]

See you next time!

References

[1] In this paper: https://docs.microsoft.com/en-us/aspnet/core/signalr/groups?view=aspnetcore-2.2 [2] https: https://github.com/AARNOLD87/SignalRWithAngular [3] //github.com/AARNOLD87/SignalRWithAngular: https://github.com/AARNOLD87/SignalRWithAngular

Posted on Wed, 10 Nov 2021 15:45:01 -0500 by Drayton