views:

1054

answers:

2

I have an ASP.NET MVC application that currently uses the WebClient class to make a simple call to an external web service from within a controller action.

Currently I am using the DownloadString method, which runs synchronously. I have run into issues where the external web service is unresponsive, which results in my entire ASP.NET application being thread-starved and unresponsive.

What is the best way to fix this problem? There is an DownloadStringAsync method, but I'm unsure of how to call that from the controller. Do I need to use the AsyncController class? If so, how does the AsyncController and the DownloadStringAsync method interact?

Thanks for the help.

+1  A: 

The DownloadStringAsync method uses an event model, raising the DownloadStringCompleted when it has finished. You can also stop the request if it is taking too long by calling WebClient.CancelAsync(). This will let your main request thread and your WebClient thread to run in parallel, and allow you to decide exactly how long you want your main thread to wait before returning.

In the example below, we initiate the download and set the event handler we want invoked when it is finished. The DownloadStringAsync returns immediately, so we can continue processing the rest of our request.

To demonstrate a more granular control over this operation, when we reach the end of our controller action, we can check to see if the download is complete yet; if not, give it 3 more seconds and then abort.

string downloadString = null;

ActionResult MyAction()
{
    //get the download location
    WebClient client = StartDownload(uri);
    //do other stuff
    CheckAndFinalizeDownload(client);
    client.Dispose();
}

WebClient StartDownload(Uri uri)
{
    WebClient client = new WebClient();
    client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(Download_Completed);
    client.DownloadStringAsync(uri);
    return client;
}

void CheckAndFinalizeDownload(WebClient client)
{
    if(this.downloadString == null)
    {
        Thread.Sleep(3000);
    }
    if(this.downloadString == null)
    {
        client.CancelAsync();
        this.downloadString = string.Empty;
    }
}

void Download_Completed(object sender, DownloadStringCompletedEventArgs e)
{
    if(!e.Cancelled && e.Error == null)
    {
        this.downloadString = (string)e.Result;
    }
}
Rex M
I believe this approach will still block the request thread though - you're still on the request thread during your Sleep call.Async controllers will help here because they free the request thread up while you're waiting for the external resource.
Michael Hart
@Michael indeed. This approach will run in parallel with the request thread unless the WebClient request is not finished by the time the controller thread has done everything else it needs to do - at which point the controller chooses whether to block/sleep or continue. I did not get the impression that the question is specifically around how to use the AsyncController, but rather how to make the WebClient work asynchronously. That may be an incorrect reading of the question.
Rex M
@Rex Yeah, I think from the description of the web app being "thread-starved and unresponsive" that it's likely the OP is running out of request threads. Limiting the external response to 3 secs will help a little (if your external requests were previously taking longer than that), but that's still quite a long time to be holding up the thread and hence it wouldn't scale.
Michael Hart
+2  A: 

I think using AsyncControllers will help you here as they offload the processing off the request thread.

I'd use something like this (using the event pattern as described in this article):

public class MyAsyncController : AsyncController
{
    // The async framework will call this first when it matches the route
    public void MyAction()
    {
        // Set a default value for our result param
        // (will be passed to the MyActionCompleted method below)
        AsyncManager.Parameters["webClientResult"] = "error";
        // Indicate that we're performing an operation we want to offload
        AsyncManager.OutstandingOperations.Increment();

        var client = new WebClient();
        client.DownloadStringCompleted += (s, e) =>
        {
            if (!e.Cancelled && e.Error == null)
            {
                // We were successful, set the result
                AsyncManager.Parameters["webClientResult"] = e.Result;
            }
            // Indicate that we've completed the offloaded operation
            AsyncManager.OutstandingOperations.Decrement();
        };
        // Actually start the download
        client.DownloadStringAsync(new Uri("http://www.apple.com"));
    }

    // This will be called when the outstanding operation(s) have completed
    public ActionResult MyActionCompleted(string webClientResult)
    {
        ViewData["result"] = webClientResult;
        return View();
    }
}

And make sure you setup whatever routes you need, eg (in Global.asax.cs):

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapAsyncRoute(
            "Default",
            "{controller}/{action}/{id}",
            new { controller = "Home", action = "Index", id = "" }
        );
    }
}
Michael Hart