6

I'm trying to use SignalR for Asp Net Core 2.1 in order to send a message from a controller method which call is triggered from a test button in Angular.
The behavior I'd expect is that when I click the button, my service invokes the controller method, which sends the test message. Then, I will simply log the message.

I want to manage this in a service in order to avoid code duplication in all of the components.

I've read some examples like this question about using SignalR in a service (I've used the second solution) and this article and the official docs but even with applying these concepts it don't seems to work.
(So, or I'm absolutely applying them in a wrong way or there's still something missing but I can't find out what...)

The client connects to the Message Hub successfully and if I click the button, the method is getting hit but I don't get any message and instead I get this warning in the Chrome console:

Warning: No client method with the name 'SendAsync' found.

Sending messages works fine, the issue is just with receiving them...

The question is: what am I doing wrong? Is the error on the back-end side or in the Angular side?


I share with you all of my code (the button and the service to call the controller method are not relevant since the call to the service goes fine):

> Startup.cs

public void ConfigureServices(IServiceCollection services)
{
   //...
   services.AddSignalR();
}
//...
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   //...
   app.UseSignalR(routes =>
   {
      //...
      routes.MapHub<MessageHub>("/messagehub");
      //...
   });
}

> MessageHub.cs

public class MessageHub : Hub<ITypedHubClient>
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

public interface ITypedHubClient
{
    Task SendAsync(string title, string name, string message);
}

> MessageController.cs

IHubContext<MessageHub, ITypedHubClient> _messageHubContext;

public MessageController(IHubContext<MessageHub, ITypedHubClient> messageHubContext)
{
    _messageHubContext = messageHubContext;
}

[HttpPost("Test")]
public async Task<IActionResult> Test()
{
    try
    {
        await _messageHubContext.Clients.All.SendAsync("ReceiveMessage","test", "test");

        return Ok(true);
    }
    catch (Exception e)
    {
        return BadRequest(e);
    }
}

> communication.service.ts

@Injectable()
export class CommunicationService {

  private _hubConnection: HubConnection | undefined;
  public async: any;
  message = '';
  messages: string[] = [];

  private newmessage = new Subject<string>();
  message$ = this.newmessage.asObservable();


  constructor() {
    this._hubConnection = new signalR.HubConnectionBuilder()
      .withUrl('/messagehub')
      //.configureLogging(signalR.LogLevel.Information)
      .configureLogging(signalR.LogLevel.Debug)
      .build();

    this._hubConnection.start().catch(err => console.error(err.toString()));

    this._hubConnection.on('SendMessage', (user: any, message:any) => {
      const received = `Received: ${message}`;
      //this.messages.push(received);
      this.newmessage.next(received);
      console.log("got something new...", received);
    });
  }

  clear() {
    this.newmessage.next("");
  }

  public sendMessage(): void {
    const data = `Sent: ${this.message}`;

    if (this._hubConnection) {
      this._hubConnection.invoke('SendMessage', 'AAA' ,data);
    }
    this.messages.push(data);
  }
}
Deadpool
  • 1,031
  • 3
  • 19
  • 35
  • 1
    I'm not sure were your error could come from. But as a general rule having a controller call the hub to send a message back to the client is not optimal since your doing an HTTP request instead of a websocket message. So unless you have to use the controller to do something before sending a message back to the client, it would be better to use this._hubConnection.invoke("SendMessage", ...) when the user click on the button instead of doing a post – J.Loscos May 30 '18 at 16:02
  • 1
    Attach event listener (`.on`) before starting connection (`.start`). – aaron May 31 '18 at 01:52
  • @Deadpool I thought I used the same code as you, but after double checking I realize that I didn't use a typed Hub. My hub is public class MessageHub : Hub and the context is IHubContext messageHubContext I don't have time right now to test with a typed Hub, I'll try later to see if I get the same error as you – J.Loscos May 31 '18 at 06:44
  • 1
    I got the synthax of strongly typed hub mixed up because you used SendAsync as the name of your client action, which is also the name of the method in the non strongly typed hub cases. So with _hubConnection.on('SendAsync' in the client it should work. But I recomend that you use another name than SendAsyc to avoid other mix up between the strongly typed and non strongly typed uses. – J.Loscos May 31 '18 at 08:34

1 Answers1

12

In signalr core 2.1 you can use strongly typed hubs to declare in an interface what actions can be called on the clients :

public class MessageHub : Hub<ITypedHubClient>
{
    public async Task SendMessage(string title, string user, string message)
    {
        await Clients.All.SendMessageToClient(title, user, message);
    }
}
public interface ITypedHubClient
{
    Task SendMessageToClient(string title, string name, string message);
}

in the controller :

    IHubContext<MessageHub, ITypedHubClient> _messageHubContext;

    public async Task<IActionResult> Test()
    {
        await _messageHubContext.Clients.All.SendMessageToClient("test", "test", "test");
        return Ok("ok");
    }

in the client :

_hubConnection.on('SendMessageToClient', (title, user, message) => {
    const received = `title: ${title}, name: ${user}, message: ${message}`;
    console.log(received);
});

If you don't use strongly typed hub, then to call the same method in the client it becomes :

public class MessageHub : Hub
{
    public async Task SendMessage(string title, string user, string message)
    {
        await Clients.All.SendAsync("SendMessageToClient", title, user, message);
    }
}

In that case you can use the SendAsync method on the client proxy, it's first parameter is the name of the method you want to call.

Update : When we define a strongly typed hub with an interface, all interface methods must return a Task. With custom methods, signalr generates methods that call SendCoreAsync. That allow us to call these methods asynchronously.

If the return type of the interface methods is not a Task we get the error : All client proxy methods must return 'System.Threading.Tasks.Task'

J.Loscos
  • 2,478
  • 1
  • 10
  • 17
  • It works! Thanks you very much! I add here in the comment also the method to send messages from angular frontend, which now is: `public sendMessage(): void { const data = `Sent: ${this.message}`; if (this._hubConnection) { this._hubConnection.invoke('SendMessage', 'AAA' ,'BBB', 'CCC'); } this.messages.push(data); }` – Deadpool May 31 '18 at 09:03
  • @Deadpool...Doesn't "IHubContext _messageHubContext;" need to be registered in ConfigureServices for the DI to work? – Big Daddy Jan 04 '19 at 16:07
  • @Deadpool...as a follow-up to my last question, I answered my own question. The answer is no. SignalR or Core must handle the DI internally. – Big Daddy Jan 04 '19 at 16:24