How to use async/await with hub.On in SignalR client

I know this is old, but the accepted answer creates a lambda that is async void.

But async void methods can crash your app if there's an unhandled exception. Read here and here.

Those articles do say that async void is allowed only because of events, and these are events we're talking about. But it's still true that an exception can crash your whole app. So if you are going to it, make sure you have try/catch blocks anywhere an exception could possibly be thrown.

But async void methods can also cause unexpected behaviour because the code that calls it is not waiting for it to complete before going off and doing something else.

Remember that the benefit of await is that ASP.NET can go off and do something else and come back to the rest of the code later. Usually that's good. But in this specific case, it can mean that two (or more) incoming messages can get processed at the same time and it's a toss up for which ones finishes first (the first that finishes getting processed may not be the first one that came in). Although that may or may not matter in your case.

You might be better off just waiting for it:

_hub.On<Message>("SendMessageToClient",
                 i => OnMessageFromServer(i.Id, i.Message).GetAwaiter().GetResult());

See here and here for the benefit of using .GetAwaiter().GetResult() rather than .Wait().


The SignalR client is designed to call the handler methods sequentially, without interleaving. "SingleThreaded", in other words. You can normally design the signalR client code relying on all the handler methods being called "SingleThreaded". (I use "SingleThreaded" in quotes because ... it's not single threaded, but we don't seem to have language for expressing async methods called sequentially without interleaving in a conceptually single=threaded manner)

However the "async-void" method being discussed here breaks this design assumption and causes the unexpected side-effect that the client handler methods are now called concurrently. Here's the example of code that causes the side-effect:

/// Yes this looks like a correct async method handler but the compiler is
/// matching the connection.On<int>(string methodName, Action<int> method)
/// overload and we get the "async-void" behaviour discussed above
connection.On<int>(nameof(AsyncHandler), async (i) => await AsyncHandler(i)));

/// This method runs interleaved, "multi-threaded" since the SignalR client is just
/// "fire and forgetting" it.
async Task AsyncHandler(int value) {
    Console.WriteLine($"Async Starting {value}");
    await Task.Delay(1000).ConfigureAwait(false);
    Console.WriteLine($"Async Ending {value}");
}

/* Example output:
Async Starting 0
Async Starting 1
Async Starting 2
Async Starting 3
Async Starting 4
Async Starting 5
Async Starting 6
Async Starting 7
Async Starting 8
Async Ending 2
Async Ending 3
Async Ending 0
Async Ending 1
Async Ending 8
Async Ending 7
*/

If you're using ASP.NET Core, we can attach asynchronous method handlers and have the client call them one at a time, sequentially, without interleaving, without blocking any threads. We utilize the following override introduced in SignalR for ASP.NET Core.

IDisposable On(this HubConnection hubConnection, string methodName, Type[] parameterTypes,
                Func<object[], Task> handler)

Here's the code that achieves it. Woefully, the code you write to attach the handler is a bit obtuse, but here it is:

/// Properly attaching an async method handler
connection.On(nameof(AsyncHandler), new[] { typeof(int) }, AsyncHandler);

/// Now the client waits for one handler to finish before calling the next.
/// We are back to the expected behaviour of having the client call the handlers
/// one at a time, waiting for each to finish before starting the next.
async Task AsyncHandler(object[] values) {
    var value = values[0];
    Console.WriteLine($"Async Starting {value}");
    await Task.Delay(1000).ConfigureAwait(false);
    Console.WriteLine($"Async Ending {value}");
}

/* Example output
Async Starting 0
Async Ending 0
Async Starting 1
Async Ending 1
Async Starting 2
Async Ending 2
Async Starting 3
Async Ending 3
Async Starting 4
Async Ending 4
Async Starting 5
Async Ending 5
Async Starting 6
Async Ending 6
Async Starting 7
Async Ending 7
*/

Of course, now you know how to achieve either kind of client behaviour depending on your requirements. If you choose to use the async-void behaviour, it would be best to comment this really really well so you don't trap other programmers, and make sure you don't throw unhandled task exceptions.


This is a void-awaitable pattern, use it like this:

_hub.On<Message>("SendMessageToClient", async i => await OnMessageFromServer(i.Id, i.Message))