3

I've got an architectural problem I just can't find a suitable solution for. In my web application based on ASP.NET Core MVC 2.2, I want to pull data from a JWT secured API and publish it to connected clients using SignalR. Furthermore, the data should only be fetched, if at least one client is actually connected to the SignalR hub. My problem: I can't find a way to cancel the Task.Delay inside a while loop, when an additional client's connected. To clarify, let me show you what I came up with so far.

First of all, here's the API client class:

public class DataApiClient : IDataApiClient {

    private readonly HttpClient httpClient;
    private readonly string dataApiDataUrl;

    public DataApiClient(HttpClient httpClient, IOptionsMonitor<DataSettings> dataSettings) {
        this.httpClient = httpClient;
        dataApiUrl = dataSettings.CurrentValue.dataApiUrl;
    }

    public async Task<DataOverview> GetData(string accessToken) {

        DataOverview dataOverview = new DataOverview();

        try {
            httpClient.DefaultRequestHeaders.Accept.Clear();
            // more httpClient setup

            Task<Stream> streamTask = httpClient.GetStreamAsync(dataApiUrl);
            dataOverview = serializer.ReadObject(await streamTask) as DataOverview;

        } catch(Exception e) {
            Debug.WriteLine(e.Message);
        }
        return dataOverview;
    }
}

SignalR hub:

public interface IDataClient {
    Task ReceiveData(DataOverview dataOverview);
}

public class DataHub : Hub<IDataClient> {

    private volatile static int UserCount = 0;

    public static bool UsersConnected() {
        return UserCount > 0;
    }

    public override Task OnConnectedAsync() {
        Interlocked.Increment(ref UserCount);
        return base.OnConnectedAsync();
    }

    public override Task OnDisconnectedAsync(Exception exception) {
        Interlocked.Decrement(ref UserCount);
        return base.OnDisconnectedAsync(exception);
    }
}

And a BackgroundService that gets the work done:

public class DataService : BackgroundService {

    private readonly IHubContext<DataHub, IDataClient> hubContext;
    private readonly IDataApiClient dataApiClient;
    private readonly IAccessTokenGenerator accessTokenGenerator;
    private AccessToken accessToken;

    public DataService(IHubContext<DataHub, IDataClient> hubContext, IDataApiClient dataApiClient, IAccessTokenGenerator accessTokenGenerator) {
        this.hubContext = hubContext;
        this.dataApiClient = dataApiClient;
        this.accessTokenGenerator = accessTokenGenerator;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {

        while (!stoppingToken.IsCancellationRequested) {

            if (DataHub.UsersConnected()) {

                if (NewAccessTokenNeeded()) {
                    accessToken = accessTokenGenerator.GetAccessToken();
                }

                DataOverview dataOverview = dataApiClient.GetData(accessToken.Token).Result;
                dataOverview.LastUpdated = DateTime.Now.ToString();

                await hubContext.Clients.All.ReceiveData(dataOverview);
            }
            // how to cancel this delay, as soon as an additional user connects to the hub?
            await Task.Delay(60000);
        }
    }

    private bool NewAccessTokenNeeded() {
        return accessToken == null || accessToken.ExpireDate < DateTime.UtcNow;
    }
}

So my questions are:

  1. My main problem: How can I cancel the Task.Delay() inside the ExecuteAsync() while loop the moment an additional user connects, so the newly connected client gets data immediately and doesn't have to wait until the Delay() task is over? I guess, this would have to be placed in OnConnectedAsync(), but calling the service from there doesn't seem to be a good solution.

  2. Is this architecture even good? If not, how would you implement such a scenario?

  3. Is there a better way to keep a count of currently connected SignalR users? I read that a static property in a Hub can be problematic, if more than one SignalR server is involved (but this is not case for me).

  4. Does IHostedService/BackgroundService even make sense here? Since services that get added using AddHostedService() are transient now, doesn't the while loop inside the ExecuteAsync() method defeat the purpose of this approach?

  5. What would be the best place to store a token such as JWT access tokens, so that transient instances can access it if it's valid and update it when it expired?

I also read about injecting a reference to a specific IHostedService, but that seems to be just wrong. Also this discussion on Github made me feel that there has to be a better way to design the communication between SignalR and continuously running services.

Any help would be greatly appreciated.

jonas2k
  • 73
  • 8

0 Answers0