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:
- Client sends a request to the server to start the process
- Server starts processing
- Server will send a notification after each step (eg.
InQueue
,Started
,Fetched
,Processed
,Saved
,Done
) - 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);
}
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");
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();
});
});
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
- the
- 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, []);
[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();
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.