Calling Task-based methods from ASMX

前端 未结 2 955
感情败类
感情败类 2020-12-28 19:09

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.

相关标签:
2条回答
  • 2020-12-28 19:48

    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);
    }
    
    0 讨论(0)
  • 2020-12-28 19:52

    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!

    0 讨论(0)
提交回复
热议问题