问题
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!!!
回答1:
This is a void-awaitable pattern, use it like this:
_hub.On<Message>("SendMessageToClient", async i => await OnMessageFromServer(i.Id, i.Message))
回答2:
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()
.
回答3:
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.
来源:https://stackoverflow.com/questions/27485996/how-to-use-async-await-with-hub-on-in-signalr-client