4

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 ?

Emmanuel DURIN
  • 4,803
  • 2
  • 28
  • 53
  • maybe move your long-running task to something like [Hangfire](https://www.hangfire.io/) and make you signalr calls short? – vasily.sib Feb 19 '21 at 09:48
  • Does your long running op on the server side provide many good moments to cancel the operation? – Caius Jard Feb 19 '21 at 09:54
  • 2
    You are misusing the hub. The hub should be a mediator, not a workhorse, in short don't do this. . Offload the call to something stateful and suitable to running long running tasks via a message or pass through. Create a cancellation token in that environment, use a dictionary and pass back a key if you have to, cancel that token via its key if its available – TheGeneral Feb 19 '21 at 10:02
  • Thx for your help , actually, there is a separate class processing the long call. The HubContext is required in the other class to notify back the client – Emmanuel DURIN Feb 19 '21 at 10:11
  • @Caius yes there are good moments because I have a loop with many iterations and I am already scanning a CancellationToken – Emmanuel DURIN Feb 19 '21 at 10:12

1 Answers1

1

At last I got a solution

It required to have the SignalR HubContext injected in a custom notifier

It allows :

  1. to callback the Js client during the long work
  2. to have some kind of reports (callback) to client
  3. to cancel from the client side

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();
      }
    }
Emmanuel DURIN
  • 4,803
  • 2
  • 28
  • 53