angularasp.net-core-2.0asp.net-core-signalrtaskcompletionsource

SignalR workaround to get client data from server


I know that SignalR can't have a return from client when invokation came from the server. On the github repository of SignalR I asked for a workaround (https://github.com/aspnet/SignalR/issues/1329) and they suggest me to get the result by sending it from the client to server to another method in the hub and so use TaskCompletionSource and some connection metadata to catch the result, but I'm stuck on how to do this

Controller Server :

[HttpPut("send/{message}")]
public async Task<IActionResult> SendMessage(string message)
{
    if (!ModelState.IsValid) return BadRequest(ModelState.Values);

    string connectionId = Request.Headers["connectionId"];
    await _chatHubContext.Clients.Client(connectionId).InvokeAsync("send");

    // Catch the call of MessageReceived and get the chat status

    return new OkObjectResult(new EmptyJsonResult() { Result = "OK" }); 

}

Hub Server

public class ChatHub : Hub
{
    public Task MessageReceive(bool chatStatus)
    {
        // Tell controller that message is received
    }
}

Angular 4 client

import { Component, Inject } from '@angular/core';
import { HubConnection } from '@aspnet/signalr-client';

@Component({
  selector: 'chat',
  templateUrl: './chat.component.html',
  styleUrls: ['./chat.component.css']
})
/** chat component*/
export class ChatComponent {
  hubConnection: HubConnection;
  chatStatus = false;

  /** chat ctor */
  constructor( @Inject('BASE_URL') private originUrl: string) {
    this.hubConnection = new HubConnection(`${this.originUrl}chat`);

    setInterval(() => {
      this.chatStatus = !this.chatStatus;
    },
      5000);

    this.hubConnection
      .start()
      .then(() => {
        this.hubConnection.on('send', (message: string) => {
          if (this.chatStatus) {
            //send message
          }
          this.hubConnection
            .invoke('messageReceived', this.chatStatus);
        });
      });

  }
}

As you can see on this code, I don't know what to do in the controller method and the Hub method to know that the method MessageReceive was called and to get his return to send it back to the controller request.


Solution

  • "with a little hacking around with connection metadata and TaskCompletionSource you could also probably make it look a lot like a method invocation returning a value."

    Controller server:

    Inject HttpConnectionManager.

    // using Microsoft.AspNetCore.Http.Connections.Internal;
    
    public async Task<IActionResult> SendMessage(string message)
    {
        string connectionId = Request.Headers["connectionId"];
    
        var chatStatus = await Send(connectionId, message);
    
        return new OkObjectResult(new { Result = "OK", ChatStatus = chatStatus });
    }
    
    private async Task<bool> Send(string connectionId, string message)
    {
        var tcs = new TaskCompletionSource<bool>();
    
        _connectionManager.TryGetConnection(connectionId, out HttpConnectionContext connection);
    
        connection.Items.Add("tcs", tcs);
    
        await _chatHubContext.Clients.Client(connectionId).SendAsync("send", message);
    
        var chatStatus = await tcs.Task;
    
        connection.Items.Remove("tcs");
    
        return chatStatus;
    }
    

    Hub server:

    public Task MessageReceived(bool chatStatus)
    {
        Context.Items.TryGetValue("tcs", out object obj);
    
        var tcs = (TaskCompletionSource<bool>)obj;
    
        tcs.SetResult(chatStatus);
    
        return Task.CompletedTask;
    }
    

    Angular 4 client:

    // No change