Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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!!!

like image 674
Greg Grater Avatar asked Dec 15 '14 14:12

Greg Grater


People also ask

Is SignalR asynchronous?

SignalR is an asynchronous signaling library for ASP.NET that our team is working on to help build real-time multi-user web application.

What is Hub in SignalR?

Hubs are SignalR's high level API that allow both realtime client-to-server and server-to-client RPC over HTTP. The hub supports 1-to-many RPC e.g.: all clients, groups of clients, just the caller etc. Transport is over one of the following (best to worst): WebSocket, server sent events, forever frame, long polling.

How do I send a message to particular client in SignalR?

public async Task BroadcastToUser(string data, string userId) => await Clients. User(userId). SendAsync("broadcasttouser", data); Remember that when we are sending messages to a user, they will be sent to all connections associated with that user and not just any particular connection.


2 Answers

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

_hub.On<Message>("SendMessageToClient", async i => await OnMessageFromServer(i.Id, i.Message))
like image 60
AgentFire Avatar answered Sep 25 '22 13:09

AgentFire


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.

like image 27
bboyle1234 Avatar answered Sep 25 '22 13:09

bboyle1234