13

I have a .Net Windows Service (client) that's communicating with a SignalR Hub (server). Most of the client methods will take time to complete. When receiving a call from the server, how do I (or do I need to) wrap the target method/hub.On to avoid the warning:

"Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the await operator to the result of the call"

On the client, this is a sample of the start up / setup code:

IHubProxy _hub
string hubUrl = @"http://localhost/";

var connection = new HubConnection(hubUrl, hubParams);
_hub = connection.CreateHubProxy("MyHub");
await connection.Start();

_hub.On<Message>("SendMessageToClient", i => OnMessageFromServer(i.Id, i.Message));
_hub.On<Command>("SendCommandToClient", i => OnCommandFromServer(i.Id, i.Command));

Also on the client, this is a sample of the methods:

public static async Task<bool> OnMessageFromServer(string Id, string message)
{
    try
    {
        var result = await processMessage(message);  //long running task
    }
    catch (Exception ex)
    {
        throw new Exception("There was an error processing the message: ", ex);
    }
    return result;
}

public static async Task<bool> OnCommandFromServer(string Id, string command)
{
    try
    {
        var result = await processCommand(command);  //long running task
    }
    catch (Exception ex)
    {
        throw new Exception("There was an error processing the message: ", ex);
    }
    return result;
}

Ultimately, I think the _hub.On is registering the callback, not the actual execution (invoke) from the server. I think I need to get in the middle of the actual execution, await the result of On[X]FromServer and return the result.

************* updated example with corrected code*********************

IHubProxy _hub
string hubUrl = @"http://localhost/";

var connection = new HubConnection(hubUrl, hubParams);
_hub = connection.CreateHubProxy("MyHub");
await connection.Start();

//original
//_hub.On<Message>("SendMessageToClient", i => OnMessageFromServer(i.Id, i.Message));
//_hub.On<Command>("SendCommandToClient", i => OnCommandFromServer(i.Id, i.Command));

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

_hub.On<Message>("SendCommandToClient", 
    async (i) => await OnCommandFromServer(i.Id, i.Message));

//expanding to multiple parameters
_hub.On<Message, List<Message>, bool, int>("SendComplexParamsToClient", 
    async (p1, p2, p3, p4) => 
       await OnComplexParamsFromServer(p1.Id, p1.Message, p2, p3, p4));    

And then the target method signature would be something like

public static async Task<bool> OnComplexParamsFromServer(string id, string message,
                 List<Message> incommingMessages, bool eatMessages, int repeat)
{
    try
    {
        var result = await processCommand(message);  //long running task
        if (result) 
        {
             // eat up your incoming parameters
        }
    }
    catch (Exception ex)
    {
        throw new Exception("There was an error processing the message: ", ex);
    }
    return result;
}

Thanks to @AgentFire for the quick response!!!

Greg Grater
  • 2,001
  • 5
  • 18
  • 24

3 Answers3

13

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

_hub.On<Message>("SendMessageToClient", async i => await OnMessageFromServer(i.Id, i.Message))
AgentFire
  • 8,944
  • 8
  • 43
  • 90
  • Thanks for the answer!!! From a readability perspective, the "void-awaitable" that returns a bool? From the code, it doesn't look like it returns anything. Is there a better way to register the callback that might be clearer to other developers looking at my code? Or is this abundantly clear to real developers and just not clear to a newbie like me? :-) – Greg Grater Dec 15 '14 at 15:10
  • 2
    Yes, the async-void method is pretty known for everyone. There are three types of async method and... you should learn about the whole thing, actually. Won't hurt you :] – AgentFire Dec 15 '14 at 15:23
  • 4
    Your answer is the only resource on the internet for the term *void-awaitable*. I also don't understand what *async void* has to do with this, in addition to *async void* being highly discouraged. – user247702 Jul 24 '17 at 13:34
  • This is not async-void. This is async Task because `OnMessageFromServer` returns `Task` and it's perfectly fine. – bboyle1234 Jul 30 '19 at 07:33
  • @bboyle1234 It becomes clearer when you don't use anonymous functions. For example, if you were allowed to return anything from those methods, he could have just done this: `_hub.On("SendMessageToClient", OnMessageFromServer);`. But you can't. The compiler complains that the method signature doesn't match. If you change the return value of `OnMessageFromServer` to `void`, it's fine. – Gabriel Luci Jul 30 '19 at 12:11
  • @bboyle1234 You can see this in the documentation too. [`HubProxyExtensions.On`](https://learn.microsoft.com/en-us/previous-versions/aspnet/jj879500(v=vs.100)) takes an `Action` with any number of parameters. If you look at the documentation of [`Action`](https://learn.microsoft.com/en-us/dotnet/api/system.action) (and all the `Action` types, which define the number of parameters), they all say in the first line on the page, "does not return a value". An anonymous function that returns a value is a [`Func`](https://learn.microsoft.com/en-us/dotnet/api/system.func-1). – Gabriel Luci Jul 30 '19 at 12:14
  • @GabrielLuci, you are right. I've done some homework/testing and posted a thorough discussion below. Thanks – bboyle1234 Aug 01 '19 at 02:35
4

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().

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • On the other hand there are normal and async calls regarding to all of the hub methods. So, which one do you suggest? Should I use async methods for connections and the others in the Hub class? Or some of them async, the others normal? –  Jul 24 '19 at 06:39
  • @hexadecimal If you are *calling* a method, and there is an async method, then use it with `await`. That's best. But in this case, the `On()` method is not designed to accept an `async` method, which means it will not await your method if you pass it an `async` method, which might cause the issues I described. – Gabriel Luci Jul 24 '19 at 12:10
2

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.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
bboyle1234
  • 4,859
  • 2
  • 24
  • 29
  • 1
    For future readers, what you're calling "the latest version of the SignalR client", is SignalR for ASP.NET Core ([`Microsoft.AspNetCore.SignalR.Client`](https://learn.microsoft.com/en-ca/dotnet/api/microsoft.aspnetcore.signalr.client)). The latest version of SignalR for ASP.NET ([`Microsoft.AspNet.SignalR.Client`](https://learn.microsoft.com/en-us/previous-versions/aspnet/jj918088(v=vs.100))) doesn't have this method. This is important because the client version has to match the server version. You cannot use the ASP.NET Core client if the server is not ASP.NET Core. – Gabriel Luci Aug 01 '19 at 12:30
  • @GabrielLuci Good catch, feel free to edit it if you like – bboyle1234 Aug 01 '19 at 12:31
  • very interesting information! Where did you find it? Any documentation I could find is limited to how to write a hello world client and doesn't cover topics like threading at all... – curious coder Dec 31 '20 at 17:23