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.
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 = "" }
);
}
}
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With