post-thumb

How to use SignalR the basic scenario - tutorial

In the previous article, I describe the idea of SignalR and some typical scenarios where it is useful . Today, I will show you a simple but practical configuration SignalR on on both the backend and frontend. I will guide you step by step how to make SignalR working for you and your users.

The scenario

The idea of the scenario is to find a balance between simplicity and usefulness. It must be simple so as not to overshadow the main thread, but it should be a bit complicated to find benefits of using it. Let’s assume you have a long running process in the backend and you would like to inform the client about the progress. The full process looks like:

  1. Client sends a request to the server to start the process
  2. Server starts processing
  3. Server will send a notification after each step (eg. InQueue, Started, Fetched, Processed, Saved, Done)
  4. Client can show the progress to the user

How to enable SignalR on ASP.NET Core backend

Ok. We know what we want to achieve. Let’s start the configuration of the backend.

Info

The full working example you can find in the CodePruner.com repository . Check projects:

  • CodePruner.Examples.SignalR.Api
  • codepruner.examples.signalr.react.webapp

Creating Hub

The 1st thing you have to create is a Hub. The Hub is a class that inherits from Hub class. It is the place where you can define methods that will be used to send messages to the clients. The simple hub can be like:

using Microsoft.AspNetCore.SignalR;

namespace CodePruner.Examples.SignalR.Api.SignalRCode
{
    public class ProcessingHub: Hub
    {
        public void ProcessStatusUpdate(ProcessStatusUpdateMessage processStatusUpdateMessage)
        {
            Clients.All.SendAsync("ProcessStatusUpdate", processStatusUpdateMessage);
        }   
    }
}

The above example will work, but it is the standard old approach to define Hubs. In the current, civilized version you can use the better approach and use generic Hub<T> class:

using Microsoft.AspNetCore.SignalR;

namespace CodePruner.Examples.SignalR.Api.SignalRCode;

public class StronglyTypedProcessingHub : Hub<IProcessingClient>
{

}


public interface IProcessingClient
{
    Task ProcessStatusUpdate(ProcessStatusUpdateMessage processStatusUpdateMessage);
}
As you can see you need additional interface to define the contract. I really prefer the second approach. It is more readable and easier to use, because it reduces a risk of typo.

Configuring SignalR

When you have a hub, you are ready to configure the rest of the SignalR. You have to add SignalR to services in the Startup.cs or Program.cs file:

builder.Services
    .AddSignalR()
    .AddJsonProtocol(options => options
        .PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter()));

As you can see I have added an additional enum configuration: .PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter()));. It is because I want to serialize enums as strings. It is more readable for the client and it is easier to debug and maintain.

You have to also register your Hub in the middleware of the application pipeline:

app.MapHub<ProcessingHub>("/ProcessingHub");
app.MapHub<StronglyTypedProcessingHub>("/StronglyTypedProcessingHub");
I added two of them to how you that standard and generic hub are registered in the same way, but I guess in your project you will one only one. I suggest the 2nd one, because strongly typed hubs are better.

Enabling CORS

There is one more thing to do on the backend. CORS policy must be set:

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        corsPolicyOptions =>
        {
            corsPolicyOptions.WithOrigins("http://localhost:5173")
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials();
        });
});
The origin is the address of the frontend. In the real project it should be taken from the configuration.

How to connect frontend client to SignalR

When everything is configured on the backend, we can switch our attention to the frontend code.

Installing SignalR client

The 1st thing you have to do is to install the SignalR client.

npm i @microsoft/signalr

When you have installed the client package you can start using it. I prefer to create a separate file to handle the connection to the SignalR hub. Then you can use with any frontend framework like React, Angular or Vue. The example code is here:

import {
  HubConnection,
  HubConnectionBuilder,
  LogLevel,
} from "@microsoft/signalr";

const configureSignalRClient = () => {
  const hubUrl = "https://localhost:7270/StronglyTypedProcessingHub";
  console.log("[SignalConnection] Initializing SignalR connection.");

  var connection = new HubConnectionBuilder()
    .withUrl(hubUrl)
    .configureLogging(LogLevel.Information)
    .build();

  console.log("[SignalConnection] Connecting to SignalR hub");
  connection
    .start()
    .then(() => {
      console.log("[SignalConnection] Connected to SignalR hub");
    })
    .catch((err) => {
      console.log(`[SignalConnection] Error connecting to SignalR hub: ${err}`);
    });

    connection.on("ProcessStatusUpdate", (message: {processId: string, currentStatus: string}) => {
        console.log(`${message.processId} - ${message.currentStatus}`);
    });

  return connection;
};

 export const SignalRConnectionInstance: HubConnection = configureSignalRClient();

In shortcut you have to:

  • Import the SignalR client
  • Create a connection with an address to the SignalR hub
    • the hubUrl is the address of the SignalR hub and it should be taken from the configuration
  • Start the connection
  • Add listeners to handle messages from the server

Ok. We have the initialization of the SignalR. Now you have to import the connection in your application to. Here is an example of it in the React component. The example code is here:

  const connection = useMemo(() => SignalRConnectionInstance, []);
Fantastic! Now when you start the application you should see information about the connection in the console:

[SignalConnection] Initializing SignalR connection.
[SignalConnection] Connecting to SignalR hub
[SignalConnection] Connected to SignalR hub

Sending notifications from the server

There is only one missing part. When everything is configured you should send message from the server. To achieve it, create an endpoint that will be used to start the process:

app.MapPost("/StartFileProcessingSync", async (
        FileProcessor fileProcessor,
        IHubContext<StronglyTypedProcessingHub, IProcessingClient> hub) =>
    {
        var fileProcessingId = Guid.NewGuid();
        var fileProcessId = new FileProcessId(fileProcessingId);

        var actualState = new ProcessStatusState(fileProcessId, ProcessStatus.InQueue);
        while (actualState.Status != ProcessStatus.Done)
        {
            await hub.Clients.All.ProcessStatusUpdate(new ProcessStatusUpdateMessage(fileProcessId.Id,
                actualState.Status));
            await Task.Delay(2000);
            actualState = await fileProcessor.ProcessFile(fileProcessId, actualState.Status);
        }

        await hub.Clients.All.ProcessStatusUpdate(new ProcessStatusUpdateMessage(fileProcessId.Id, actualState.Status));
    })
    .WithName("FileProcessing")
    .WithOpenApi();
It imitates long run task. After sending the request it will change the status of the “process” and send notifications vis SignalR. You can notice how IHubContext is configured. When you use strongly typed hubs you should inject IHubContext<StronglyTypedProcessingHub, IProcessingClient> instead of IHubContext<ProcessingHub>. Then you will be able to use previously defined methods.

We just need to call this endpoint from the frontend. Here is an example of it in the React component::

  const handleClick = () => {
    console.log("[Click] Sending request to start processing.");
    const requestOptions = {
      method: "POST",
      headers: { "Content-Type": "application/json" },
    };

    var request = fetch(
      "https://localhost:7270/StartFileProcessingSync",
      requestOptions
    );
    request.then(() => {
      console.log(`[Click] Request is done`);
    });
  };

  return (
    <div>
      <button onClick={handleClick}>Start processing:</button>
    </div>
  );

Fantastic! Our example is ready. When you run the application and click the button you should see the information about the process in the console. It looks like:

[SignalConnection] Initializing SignalR connection.
[SignalConnection] Connecting to SignalR hub
[SignalConnection] Connected to SignalR hub
[Click] Sending request to start processing.
c32a0e10-7fc9-4179-9680-7063628d56c0 - InQueue
c32a0e10-7fc9-4179-9680-7063628d56c0 - Started
c32a0e10-7fc9-4179-9680-7063628d56c0 - Fetched
c32a0e10-7fc9-4179-9680-7063628d56c0 - Processed
c32a0e10-7fc9-4179-9680-7063628d56c0 - Saved
[Click] Request is done
c32a0e10-7fc9-4179-9680-7063628d56c0 - Done

Summary

If you want to try the example you can clone the code and run it locally.

Do you have any questions about SignalR? Just ask below.

What is the next topic you would like to read about? Let me know in the comments bellow.

comments powered by Disqus

Are you still here? Subscribe for more content!