I have a recent experience I'd like to share that may be helpful to anyone having to maintain a legacy ASMX web service that must be updated to call Task-based methods.
I've recently been updating an ASP.NET 2.0 project that includes a legacy ASMX web service to ASP.NET 4.5. As part of the update, I introduced a Web API interface to allow advanced automation of the application. The ASMX service has to co-exist with the new API for backwards-compatibility.
One of the functions of the application is to be able to request data from external data sources (industrial plant historians, bespoke web services, etc.) on behalf of the caller. As part of the upgrade, I re-wrote substantial parts of the data access layer to asynchronously request data using a Task-based asynchronous pattern. Given that it's not possible to use aync/await in an ASMX service, I modified the ASMX methods to make blocking calls to the asynchronous methods i.e. calling the Task-based method and then using Task.WaitAll to block the thread until the Task completes.
When calling any ASMX method that was calling a method returning Task or Task<T> under the hood, I found that the request always timed out. When I stepped through the code, I could see that that the asynchronous code was successfully executing, but the call to Task.WaitAll never detected that the task had completed.
This caused a major headache: how could the ASMX service happily co-exist with the new asynchronous data access capabilities?
Upon further investigation, I discovered that sub-tasks created by the initial task could be awaited without any problems, but the parent task (i.e. the method call in the ASMX that returned a Task<T>) was never detected as completing.
The investigation led me to theorise that there was some sort of incompatibility between the legacy Web Services stack and the Task Parallel Library. The solution that I came up with involves creating a new thread to run the Task-based method calls, the idea being that a separate thread would not be subject to thread/task management incompatibilities that existed in the thread processing the ASMX request. To this end I created a simple helper class that will run a Func<T> in a new thread, block the current thread until the new thread terminates and then returns the result of the function call:
Running the Task-based method in this way works perfectly and allows the ASMX call to complete successfully, but it's obviously a bit brute-force to spawn a new thread for every asynchronous call; alternatives, improvements or suggestions are welcome!
The first thing to do is ensure that
httpRuntime@targetFramework
is set to4.5
in yourweb.config
.This is actually a classic deadlock situation. I describe it in full on my blog, but the gist of it is that
await
will (by default) capture a "context" and use that to resume theasync
method. In this case, that "context" is an ASP.NET request context, which only allows one thread at a time. So, when the asmx code further up the stack blocks on the task (viaWaitAll
), it is blocking a thread in that request context, and theasync
method cannot complete.Pushing the blocking wait to a background thread would "work", but as you note it is a bit brute-force. A minor improvement would be to just use
var result = Task.Run(() => MethodAsync()).Result;
, which queues the background work to the thread pool and then blocks the request thread waiting for it to complete. Alternatively, you may have the option of usingConfigureAwait(false)
for everyawait
, which overrides the default "context" behavior and allows theasync
method to continue on a thread pool thread outside the request context.But a much better improvement would be to use asynchronous calls "all the way". (Side note: I describe this in more detail in an MSDN article on
async
best practices).ASMX does allow asynchronous implementations of the APM variety. I recommend that you first make your asmx implementation code as asynchronous as possible (i.e., using
await WhenAll
rather thanWaitAll
). You'll end up with a "core" method that you then need to wrap in an APM API.The wrapper would look something like this:
This gets a bit tedious if you have a lot of methods to wrap, so I wrote some
ToBegin
andToEnd
methods as part of my AsyncEx library. Using these methods (or your own copy of them if you don't want the library dependency), the wrappers simplify nicely: