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:
My main problem: How can I cancel the
Task.Delay()
inside theExecuteAsync()
while loop the moment an additional user connects, so the newly connected client gets data immediately and doesn't have to wait until theDelay()
task is over? I guess, this would have to be placed inOnConnectedAsync()
, but calling the service from there doesn't seem to be a good solution.Is this architecture even good? If not, how would you implement such a scenario?
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).Does
IHostedService
/BackgroundService
even make sense here? Since services that get added usingAddHostedService()
are transient now, doesn't the while loop inside theExecuteAsync()
method defeat the purpose of this approach?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.