June 10, 2019

If this sounds like something that will save you time on your projects, please do me a favour and check it out

I've been working on a new project called StellarAdmin that helps ASP.NET Core developers like you rapidly create admin screens for your application's Admin and Support users.

In ASP.NET Core 3.0, Microsoft is updating the SignalR client by adding the ability to automatically reconnect when a connection is dropped. This blog post will show you how you can use this feature.

Background

I am developing a Google Docs Add-On for Cloudpress that will allow users to publish content to their Content Management System from inside Google Docs. When a user publishes a document, the content is sent to Cloudpress where it will be processed by a background job (using Hangfire).

While the background job is being processed I want to give the user feedback on the progress, so I have implemented SignalR in the application for this purpose. I have implemented this using the same principles as I described previously in my Communicate the status of a background job with SignalR blog post.

This is what it looks like in action:

This works great, but every so often I would run into an issue with the SignalR connection dropping. When this happens, the user experience is really bad, since the status of the job will not update, and the user does not know what is happening. You can manually reconnect, but you need to be careful about it and take things like exponential back-off into account.

I am aware that automatic reconnect is being added to SignalR in ASP.NET Core 3.0 and that the PR for it was already merged a few months ago. So, I decided rather than creating my own retry mechanism, I would try and use the 3.0 preview JavaScript client with my ASP.NET 2.2 application.

It turns out that for my limited use case, it works great. Let’s see how we can configure it.

Configuring automatic reconnects

Configuring automatic reconnects only requires a call to withAutomaticReconnect on the HubConnectionBuilder . Here is what my JavaScript code looks like for configuring my connection:

connection = new signalR . HubConnectionBuilder ( ) . withUrl ( "/publish-document-job-progress" ) . withAutomaticReconnect ( ) . configureLogging ( signalR . LogLevel . Information ) . build ( ) ;

To try it out I ran the application and killed IIS after the SignalR connection was established. As you can see from the browser’s console output, the connection is lost and SignalR tries to reconnect. Once I started IIS again, the connection was reestablished.

You will notice in the screenshot above, that SignalR tried to reconnect as soon as the connection was lost. It could not reconnect at that time, so it tried again 2 seconds later at which time IIS was back up and the connection was reestablished.

By default, SignalR will try and reconnect immediately, then 2 seconds later, then again after 10 seconds and then after 30 seconds. At that time, if the connection could still not be reestablished, it will stop trying to reconnect:

You can configure the backoff period by passing an array of retry delays to the call to withAutomaticReconnect() . The default for this is [0, 2000, 10000, 30000, null] . The null value tells SignalR to stop trying. So, for example, if I wanted it to retry at 0, 1 second and 5 seconds, I can configure my HubConnectionBuilder as follows:

connection = new signalR . HubConnectionBuilder ( ) . withUrl ( "/publish-document-job-progress" ) . withAutomaticReconnect ( [ 0 , 1000 , 5000 , null ] ) . configureLogging ( signalR . LogLevel . Information ) . build ( ) ;

In my tests, it appears that it will stop retrying either when it runs out of retry delays in the array, or when it encounters a null value. So calling withAutomaticReconnect([0, 1000, 5000, null]) or withAutomaticReconnect([0, 1000, 5000,]) appears to have pretty much the same result.

Looking at the TypeScript source code for HubConnectionBuilder , it seems you can also pass a custom retry policy. The signature for that method is

public withAutomaticReconnect ( reconnectPolicy : IRetryPolicy ) : HubConnectionBuilder ;

Where IRetryPolicy is defined as

export interface IRetryPolicy { nextRetryDelayInMilliseconds ( retryContext : RetryContext ) : number | null ; } export interface RetryContext { readonly previousRetryCount : number ; readonly elapsedMilliseconds : number ; readonly retryReason : Error ; }

So, if you want to write retry logic with more complicated logic, you have that option at your disposal as well.