简体   繁体   中英

Asynchronous web service in ASP. NET MVC

I am writing an ASP.NET MVC 5 application which among others uses web services to get/process some the data.

The data flow of the app is following: MVC Action -> Service B -> ExtSvc which is async wrapper of a web service

Here are some examples:

public class ExtSvc
{       
//Convert Event based async pattern to task based async pattern:

        private Task<Response> ProcessExtRequestAsync(Request request)
        {
            TaskCompletionSource<Response> taskCompletionSource =
                AsyncServiceClientHelpers.CreateSource<Response>(request);
            ProcessRequestCompletedEventHandler handler = null;
            handler =
                (sender, e) =>
                AsyncServiceClientHelpers.TransferCompletion(
                    taskCompletionSource, 
                    e, 
                    () => e.Result, 
                    () => this.Service.ProcessRequestCompleted -= handler);
            this.Service.ProcessRequestCompleted += handler;
            try
            {
              this.Service.ProcessRequestAsync(request, taskCompletionSource);
            }
            catch (Exception)
            {
                this.Service.ProcessRequestCompleted -= handler;
                taskCompletionSource.TrySetCanceled();
                throw;
            }

                return taskCompletionSource.Task;
            }

            //Usage:

            public async Task<Response> UpdateRequest(some arguments)
            {
                //Validate arguments and create a Request object

            var response = await this.ProcessExtRequestAsync(request)
            .ConfigureAwait(false);

            return response;
        }
}

Class B is the one that uses ExtSvc in a synchronous way

public class B
{
    public ExtSvc service {get; set;}

    public Response Update(arguments)
    {
       //some logic
       var result = this.ExtSvc.UpdateRequest(arguments).Result;
       //some logic
       return result 
    }
}

Finally the MVC action (also synchronous )

public ActionResult GetResponse(int id)
{
     //some logic
     B.Update(id);
     //some logic
     return View(...);
}

The described flow throws an error

A first chance exception of type 'System.InvalidOperationException' occurred in System.Web.dll

Additional information: An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%@ Page Async="true" %>. This exception may also indicate an attempt to call an "async void" method, which is generally unsupported within ASP.NET request processing. Instead, the asynchronous method should return a Task, and the caller should await it.

on the following line of ExtSvc : this.Service.ProcessRequestAsync(request, taskCompletionSource); ProcessRequestAsync is a void method So it corresponds to:

This exception may also indicate an attempt to call an "async void" method, which is generally unsupported within ASP.NET request processing

I know that converting GetResponse MVC action to asynchronous (by using async/await) and also converting the B class that actually uses ExtSvc to be asynchronous resolves the issue.

BUT my questions is:

If I can't change the signature of B class (because of an interface it implements) to return Task<Response> instead of Response it basically means that I can't use async/await on it so how this issue could be resolved?

ProcessRequestAsync is void but it's not async void . It looks like it's an EBAP API . EBAP components generally use AsyncOperationManager / AsyncOperation , which in turn do use SynchronizationContext to notify the underlying platform of the asynchronous operation (the last link is to my MSDN article on SynchronizationContext ).

The exception you're seeing is because ASP.NET sees that notification (of the asynchronous operation starting) and says "whoa, there, fella. You're a synchronous handler! No async for you!"

Hands-down, the best approach is to make all methods asynchronous that should be asynchronous. This means B.Update should be B.UpdateAsync . OK, so there's an interface IB.Update - just change the interface to IB.UpdateAsync too. Then you're async all the way, and the code is clean.

Otherwise, you'll have to consider hacks. You could use Task.Run as @neleus suggested - that's a way of avoiding the ASP.NET SynchronizationContext so it doesn't "see" the asynchronous operation starting - but note that "ambient context" such as HttpContext.Current and page culture is lost. Or, you could (temporarily) install a new SynchronizationContext() onto the request thread - which also avoids the ASP.NET SynchronizationContext while staying on the same thread - but some ASP.NET calls assume the presence of the ASP.NET SynchronizationContext and will fail.

There's another hack you could try; it might work but I've never done it. Just make your handler return a Task<ActionResult> and use Task.FromResult to return the view: return Task.FromResult<ActionResult>(View(...)); This hack will tell ASP.NET that your handler is asynchronous (even though it's not).

Of course, all of these hacks have the primary disadvantage that you're doing sync-over-async ( this.ExtSvc.UpdateRequest(arguments).Result ), which means you'll be using one extra unnecessary thread for the duration of each request (or two threads, if you use the Task.Run hack). So you will be missing all the benefits of using asynchronous handlers in the first place - namely, scalability.

I think the error occurs because your code

this.Service.ProcessRequestAsync(request, taskCompletionSource);

actually calls SynchronizationContext's OperationStarted method that results in error as described here .

As a possible solution you can call your action on ThreadPoolSynchronizationContext

public async Task<ActionResult> GetResponse(int id)
{
     //some logic
     await Task.Run(() => { B.Update(id); });
     //some logic
     return View(...);
}

but it adds some overhead of utilizing a thread from the pool.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM