5

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 ?

5
  • maybe move your long-running task to something like Hangfire and make you signalr calls short? Commented Feb 19, 2021 at 9:48
  • Does your long running op on the server side provide many good moments to cancel the operation? Commented Feb 19, 2021 at 9: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 Commented Feb 19, 2021 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 Commented Feb 19, 2021 at 10:11
  • @Caius yes there are good moments because I have a loop with many iterations and I am already scanning a CancellationToken Commented Feb 19, 2021 at 10:12

1 Answer 1

2

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();
      }
    }
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.