Search code examples
c#asp.net-coretasksignalr-hubasp.net-core-signalr

SignalR Core two concurrent calls from same client


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 ?


Solution

  • 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();
          }
        }