16

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?

Graham Watts
  • 629
  • 5
  • 14

3 Answers3

23

I've recently been updating an ASP.NET 2.0 project that includes a legacy ASMX web service to ASP.NET 4.5.

The first thing to do is ensure that httpRuntime@targetFramework is set to 4.5 in your web.config.

the parent task (i.e. the method call in the ASMX that returned a Task) was never detected as completing.

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 the async 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 (via WaitAll), it is blocking a thread in that request context, and the async 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 using ConfigureAwait(false) for every await, which overrides the default "context" behavior and allows the async 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 than WaitAll). 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:

// Core async method containing all logic.
private Task<string> FooAsync(int arg);

// Original (synchronous) method looked like this:
// [WebMethod]
// public string Foo(int arg);

[WebMethod]
public IAsyncResult BeginFoo(int arg, AsyncCallback callback, object state)
{
  var tcs = new TaskCompletionSource<string>(state);
  var task = FooAsync(arg);
  task.ContinueWith(t =>
  {
    if (t.IsFaulted)
      tcs.TrySetException(t.Exception.InnerExceptions);
    else if (t.IsCanceled)
      tcs.TrySetCanceled();
    else
      tcs.TrySetResult(t.Result);

    if (callback != null)
      callback(tcs.Task);
  });

  return tcs.Task;
}

[WebMethod]
public string EndFoo(IAsyncResult result)
{
  return ((Task<string>)result).GetAwaiter().GetResult();
}

This gets a bit tedious if you have a lot of methods to wrap, so I wrote some ToBegin and ToEnd 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:

[WebMethod]
public IAsyncResult BeginFoo(int arg, AsyncCallback callback, object state)
{
  return AsyncFactory<string>.ToBegin(FooAsync(arg), callback, state);
}

[WebMethod]
public string EndFoo(IAsyncResult result)
{
  return AsyncFactory<string>.ToEnd(result);
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Some really useful information in there - thanks! I'd rather not make any substantial changes to web service interface itself (i.e. convert it to use the APM) but using Task.Run is certainly a nicer way of avoiding the deadlock. I've also used ConfigureAwait(false) wherever I can, although the nature of the beast is that I can't guarantee that this would always be used, since some of the data source drivers could, in theory, be written by 3rd parties in the future. – Graham Watts Jun 09 '14 at 09:04
  • If you do use `Task.Run`, then you lose *all* the benefits of async code. It's your call, of course, but do be aware that unless you make the asmx apis asynchronous (APM), then they're not getting any of the benefits of asynchronous code (such as scalability). – Stephen Cleary Jun 09 '14 at 12:05
  • Understood. At present, there are a couple of applications that use the ASMX service; the intention is to migrate these to use the Web API so right now we're just looking for compatibility more than anything else. Additionally, the ASMX has been synchronous up until now anyway, so it's a case of "not gaining" async benefits rather than losing them. New applications that make use of the web app will do so asynchronously, via the Web API. – Graham Watts Jun 10 '14 at 12:53
  • @StephenCleary Would you still recommend the same now in 2018? I am working on a similar project which uses asmx and i need to better performance. I see lines like .Result and .Wait littered around in the code base and it does not smell good. Need some advice. – Som Bhattacharyya Nov 28 '18 at 11:42
  • 2
    @SomBhattacharyya: Yes. AFAIK, asmx is a dead technology, so it hasn't been updated to use `Task` directly (and probably never will be). So you still have to use the APM-style asynchronous methods with it. Also note that async may not give you "better performance" - it depends on what you mean by that. Async will give you better *scalability* (scale further and more quickly), but it will not give you a better *response time* for every request. – Stephen Cleary Nov 29 '18 at 13:49
  • @StephenCleary Thank you so much for responding.I think i understand what you are saying about async. Would you suggest keeping calls like .Result and .Wait in a asmx web method ? I am not comfortable with that considering that asmx is old tech and we are not using APM expressions to perform async anyway. – Som Bhattacharyya Nov 29 '18 at 15:13
  • 2
    @SomBhattacharyya: If you need the scalability, then use APM in asmx. Otherwise, a blocking call like `GetAwaiter().GetResult()` is your only option for calling asynchronous code. – Stephen Cleary Nov 29 '18 at 16:40
  • @StephenCleary Thanks for clarifying. So i can call 'GetAwaiter().GetResult()' from within a ASMX web method without any risk of a deadlock ? – Som Bhattacharyya Nov 30 '18 at 06:07
  • 2
    @SomBhattacharyya: No; you'll still have that deadlock risk no matter how you block. – Stephen Cleary Nov 30 '18 at 23:13
  • Okk. Thanks for the help. This will help in finding a new direction(technical debt) for our function. – Som Bhattacharyya Dec 03 '18 at 07:08
5

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:

public class ThreadRunner<T> {
    // The function result
    private T result;

    //The function to run.
    private readonly Func<T> function;

    // Sync lock.
    private readonly object _lock = new object();


    // Creates a new ThreadRunner<T>.
    public ThreadRunner(Func<T> function) {
        if (function == null) {
            throw new ArgumentException("Function cannot be null.", "function");
        }

        this.function = function;
    }


    // Runs the ThreadRunner<T>'s function on a new thread and returns the result.
    public T Run() {
        lock (_lock) {
            var thread = new Thread(() => {
                result = function();
            });

            thread.Start();
            thread.Join();

            return result;
        }
    }
}

// Example:
//
// Task<string> MyTaskBasedMethod() { ... }
//
// ...
//
// var tr = new ThreadRunner<string>(() => MyTaskBasedMethod().Result);
// return tr.Run();

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!

Graham Watts
  • 629
  • 5
  • 14
1

This may be an old topic but it contains the best answer I have been able to find to help maintain legacy code using ASMX and WebMethod to call newer async functions synchronously.

I am new to contributing to stackoverflow so I don't have the reputation to post a comment to Graham Watts solution. I shouldn't really be responding to another answer - but what other choice do I have.

Graham's answer has proved to be a good solution for me. I have a legacy application that is used internally. Part of it called an external API which has since been replaced. To use the replacement the legacy app was upgraded to .NET 4.7 as the replacement uses Tasks extensively. I know the "right" thing to do would be to rewrite the legacy code but there is no time or budget for such an extensive exercise.

The only enhancement I had to make was to capture exceptions. This may not be the most elegant solution but it works for me.

 public class ThreadRunner<T>
    {
        // Based on the answer by graham-watts to :
        // https://stackoverflow.com/questions/24078621/calling-task-based-methods-from-asmx/24082534#24082534

        // The function result
        private T result;

        //The function to run.
        private readonly Func<T> function;

        // Sync lock.
        private readonly object _lock = new object();


        // Creates a new ThreadRunner<T>.
        public ThreadRunner(Func<T> function)
        {
            if (function == null)
            {
                throw new ArgumentException("Function cannot be null.", "function");
            }

            this.function = function;
        }
        Exception TheException = null;

        // Runs the ThreadRunner<T>'s function on a new thread and returns the result.
        public T Run()
        {
            lock (_lock)
            {
                var thread = new Thread(() => {
                    try
                    {
                        result = function();
                    }catch(Exception ex)
                    {
                        TheException = ex;
                    }
                
                });

                thread.Start();
                thread.Join();

                if (TheException != null)
                    throw TheException;
                return result;
            }
        }
    }

    // Example:
    //
    // Task<string> MyTaskBasedMethod() { ... }
    //
    // ...
    //
    // var tr = new ThreadRunner<string>(() => MyTaskBasedMethod().Result);
    // return tr.Run();
DougPi
  • 33
  • 6