Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling Task-based methods from ASMX

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?

like image 863
Graham Watts Avatar asked Jun 06 '14 09:06

Graham Watts


People also ask

What is the difference between Asmx and SVC?

When web developers talk about . asmx, they know that it is something not too complicated – it can be configured easily. The downside is, it is only callable from HTTP, while . svc is used with HTTP, MSMQ, and can be hosted through different platforms such as the Winforms application.

How do you call a WebService method asynchronously in C#?

Asynchronous web service execution requires a two-step process. First, you tell the Web service to begin execution by calling the Begin method. The second step, calling the End method, completes the web service call and returns the response. To call a web service asynchronously, a client thread can use a WaitHandle.

What is Asmx file How can you deploy your web service?

What is an ASMX file? A file with . asmx extensions is an ASP.NET Web Service file that provides communication between two objects over the internet using the Simple Object Access Protocol (SOAP). It is deployed as a service on the Windows-based Web Server to process incoming request and return the response.


2 Answers

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);
}
like image 93
Stephen Cleary Avatar answered Oct 18 '22 02:10

Stephen Cleary


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!

like image 45
Graham Watts Avatar answered Oct 18 '22 03:10

Graham Watts