How to change async method call to prevent forcing

2019-07-05 13:57发布

问题:

If I need to call a method that in turn calls some async method internally, as a fire and forget operation, how can I prevent this call from forcing the "async" to need to be used up the call stack to say...an MVC controller?

For example: My MVC Controller (not async) calls a business layer method, which in turn calls the Windows Azure Service Bus QueueClient.SendAsync(BrokeredMessage), to drop a message in a queue, but doesn't need to wait for it to complete.

Typically, when this controller action is invoked, the compiler will throw an error that the async operation cannot be started at this time.

I know that instead of awaiting or just invoking the SendAsync() method, I could follow it up with ContinueWith(), in order to execute code on the callback of the async operation, but I've been told this not a correct solution. (see responses to Calling async method in controller)

Would someone care to enlighten me on the best way to fix this scenario? And tell me why the ContinueWith() approach is not correct?

回答1:

calls the Windows Azure Service Bus QueueClient.SendAsync(BrokeredMessage), to drop a message in a queue, but doesn't need to wait for it to complete.

First, I'd reconsider this assumption. If you want a fully reliable system, the action should wait for the message to be sent to the bus. Note that this is usually a fast operation (100ms).

Typically, when this controller action is invoked, the compiler will throw an error that the async operation cannot be started at this time.

Ensure that you do not have any async void methods or EAP method calls. I would be very surprised if the Azure storage library causes that exception.

Would someone care to enlighten me on the best way to fix this scenario? And tell me why the ContinueWith() approach is not correct?

The best solution is to embrace async. ContinueWith could work but is dangerous to use (it has lots of parameters, some of which have unsafe default values); await is practically the same as ContinueWith but without the dangerous defaults.


However, if 100ms is truly unbearable, and you are willing to give up reliability (in this case, that means you accept the fact that some messages may not get sent to the bus even though the action completed successfully so the client thinks that they did), then you can use the BackgroundTaskManager from my blog to minimize the chance of lost messages.



回答2:

An MVC controller method handles an HTTP request and sends back the corresponding HTTP response to the client. If I understood your question correctly, you want to call a fire-and-forget method from your async MVC controller method, then proceed with the HTTP response delivery, so the fire-and-forget method doesn't hold the response.

Indeed, you cannot fire it from with your controller method this way, without awaiting its results. I.e., you could, but if your ASP.NET application would be restarted, or the IIS server taken out of the farm, your ContinueWith callback would never be called. So, you woudn't know if the request had ever reached the Windows Azure Service.

One approach to solve this is to run a helper Web API or WCF service on the same host, or on another host on the same network (so the turn-around for a service call would be really quick). It can be a self-hosted service. You'd call this helper service from your MVC controller just to queue the fire-and-forget operation. You'd await the result of this call, but it's not a problem in this case, because both the caller and the callee of this operation would exist on the same network.

This way, the original MVC HTTP response won't be put on hold. Inside the helper service, you'd then do await QueueClient.SendAsync() to call Windows Azure Bus and process the result of this operation accordingly.