简体   繁体   中英

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?

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

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM