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();
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.