1 I have a long task (3mn) triggered by a Js client to an Asp.Net Core SignalR Hub
It works fine :
public class OptimizerHub : Hub, IOptimizerNotification
{
public async Task Optimize(LightingOptimizationInput lightingOptimizationInput)
{
LightingOptimizer lightingOptimizer = CreateLightingOptimizer();
Task t = lightingOptimizer.Optimize(lightingOptimizationInput);
await t;
}
}
2 The server callbacks the client to notify progress, messages, ...
Clients.Caller.SendAsync(nameof(OnProgress), progress);
It works fine so far.
3 I want the task to be cancellable with a client call to a Hub method
public Task Cancel()
{
GetContextLightingOptimizer()?.Cancel();
return Task.FromResult(0);
}
4 The problem
When the client makes the call, I see it go to the server in Chrome developer tools detail. The call doesn't get to the server before the end long task ends (3mn) !
5 I have tried many solutions
Like changing my long task call method, always failing :
// Don't wait end of task, fails because the context disappear and can't call back the client :
// Exception : "Cannot access a disposed object"
public async Task Optimize(LightingOptimizationInput lightingOptimizationInput)
{
LightingOptimizer lightingOptimizer = CreateLightingOptimizer();
Task t = lightingOptimizer.Optimize(lightingOptimizationInput);
}
6 Possible solutions
The only solution I imagine right now is the client making an Http call in a Http controller, passing a connection id that could make the cancellation.
That post provides information about a possible solution : Call SignalR Core Hub method from Controller
7 Questions
Is there a simple way that the client makes a second call to the hub while a first call is being processed ?
There is also a post giving about concurrent calls : SignalR multiple concurrent calls from client
Should I deduce from the previous post that even if my hub server method can make calls many times to the client, it can't process any other call from the client ?
At last I got a solution
It required to have the SignalR HubContext injected in a custom notifier
It allows :
Here are the steps
1 Add a notifier object whose job is to callback the Js client
Make the HubContext to be injected by the Dependency Injection
// that class can be in a business library, it is not SignalR aware
public interface IOptimizerNotification
{
string? ConnectionId { get; set; }
Task OnProgress(long currentMix, long totalMixes);
}
// that class has to be in the Asp.Net Core project to use IHubContext<T>
public class OptimizerNotification : IOptimizerNotification
{
private readonly IHubContext<OptimizerHub> hubcontext;
public string? ConnectionId { get; set; }
public OptimizerNotification(IHubContext<OptimizerHub> hubcontext)
{
this.hubcontext = hubcontext;
}
#region Callbacks towards client
public async Task OnProgress(long currentMix, long totalMixes)
{
int progress = (int)(currentMix * 1000 / (totalMixes - 1));
await hubcontext.Clients.Client(ConnectionId).SendAsync(nameof(OnProgress), progress);
}
#endregion
}
2 Register the notifier object in the Dependency Injection system
In startup.cs
services.AddTransient<IOptimizerNotification, OptimizerNotification>();
3 Get the notifier object to be injected in the worker object
public IOptimizerNotification Notification { get; set; }
public LightingOptimizer(IOptimizerNotification notification)
{
Notification = notification;
}
4 Notify from the worker object
await Notification.OnProgress(0, 1000);
5 Start Business object long work
Register business object (here it's LightingOptimizer) with a SignalR.ConnectionId so that business object can be retrived later
public class OptimizerHub : Hub
{
private static Dictionary<string, LightingOptimizer> lightingOptimizers = new Dictionary<string, LightingOptimizer>();
public async void Optimize(LightingOptimizationInput lightingOptimizationInput)
{
// the business object is created by DI so that everyting gets injected correctly, including IOptimizerNotification
LightingOptimizer lightingOptimizer;
IServiceScopeFactory factory = Context.GetHttpContext().RequestServices.GetService<IServiceScopeFactory>();
using (IServiceScope scope = factory.CreateScope())
{
IServiceProvider provider = scope.ServiceProvider;
lightingOptimizer = provider.GetRequiredService<LightingOptimizer>();
lightingOptimizer.Notification.ConnectionId = Context.ConnectionId;
// Register connectionId in Dictionary
lightingOptimizers[Context.ConnectionId] = lightingOptimizer;
}
// Call business worker, long process method here
await lightingOptimizer.Optimize(lightingOptimizationInput);
}
// ...
}
**6 Implement Cancellation in the hub **
Retrieve business object from (current) connectionId and call Cancel on it
public class OptimizerHub : Hub
{
// ...
public Task Cancel()
{
if (lightingOptimizers.TryGetValue(Context.ConnectionId, out LightingOptimizer? lightingOptimizer))
lightingOptimizer.Cancel();
return Task.FromResult(0);
}
}
7 React to Cancellation in Business object
public class LightingOptimizer
{
private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private CancellationToken cancellationToken;
public LightingOptimizer( IOptimizerNotification notification )
{
Notification = notification;
cancellationToken = cancellationTokenSource.Token;
}
public void Cancel()
{
cancellationTokenSource.Cancel();
}
public async Task Optimize(LightingOptimizationInput lightingOptimizationInput)
{
for( int i+; i < TooMuchToBeShort ;i++)
{
if (cancellationToken.IsCancellationRequested)
throw new TaskCanceledException();
}
}