Lately I wrote about how to make a lot of asynchronous calls to web services during ASP.NET page processing. Now it’s time to make same thing work with ASP.NET MVC. This blog post shows you how to use asynchronous controllers and actions in ASP.NET MVC and also you will see more complex scenario where we need to gather results of different web service calls to one result set.
Asynchronous controllers
ASP.NET MVC has its own mechanism for asynchronous calls. I don’t know why but it seems strange when you start with it and completely logical when you finish. So prepare yourself for disappointment – here is nothing complex. My experiment was mainly guided by MSDN Library page Using an Asynchronous Controller in ASP.NET MVC.
As a first thing let’s compare two controllers that have basically same meaning and in default case also the same behavior. First one is our classic synchronous controller.
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
And here is asynchronous version of same thing.
public class HomeController : AsyncController
{
public void IndexAsync()
{
}
public ActionResult IndexCompleted()
{
return View();
}
}
In the case of asynchronous controller we have to use Async and Completes prefixes for same controller methods. I mean, instead of one method we have two methods – one that starts asynchronous processing and the other that is called when all processing is done.
AsyncManager
ASP.NET MVC solves asynchronous processing using its own mechanism. There is one thing we cannot miss and it is class called AsyncManager. From documentation we can read that this class provider asynchronous operations to controller. Well, not much said, but let’s see how we will use it. One thing that seems a little bit dangerous to me is that we have to deal with some kind of counter. Take a look at the following code and see how Increment() and Decrement() methods are called.
public class HomeController : AsyncController
{
public void IndexAsync()
{
AsyncManager.OutstandingOperations.Increment();
var service = new DelayedHelloSoapClient();
service.HelloWorldCompleted += HelloWorldCompleted;
service.HelloWorldAsync(delay);
}
public ActionResult IndexCompleted()
{
return View(result);
}
void HelloWorldCompleted(object sender, HelloWorldCompletedEventArgs e)
{
AsyncManager.OutstandingOperations.Decrement();
}
}
These methods belong to OperationCounter class that maintains count of pending asynchronous operations. Increment() method raises count and Decrement()decreases it. Why I don’t like the need to call these methods is the fact that there calls are easy to forget. Believe, I was more than once able for it during five minutes. Forget these methods and you may see mysterious and hard to catch errors.
Parameters collection
AsyncManager has also parameters collection. This is solution I like a lot because instead of keeping up some controller-wide field I can save my values from asynchronous calls to AsyncManager and therefore I avoid mess in my controller code.
Parameters collection is brilliant idea in my opinion. Parameters are indexed by name and when calling completed-part of controller action those parameters are given to this method as method arguments. This is how I add results to AsyncManager parameters collection.
if (AsyncManager.Parameters.ContainsKey("result"))
{
list = (List<string>)AsyncManager.Parameters["result"];
}
else
{
list = new List<string>();
AsyncManager.Parameters["result"] = list;
}
In my case I have only one parameter, named as result and this is how my completed-method looks.
public ActionResult IndexCompleted(List<string> result)
{
return View(result);
}
Asynchronous methods fill the list like shown in previous code fragment. And parameter called result is given to IndexCompleted() method. My job was only to get the values (and of course keep in mind those damn decrements).
Example of asynchronous controller
Here is the source of my asynchronous controller. In the IndexAsync() method I create 100 calls to my dummy web service. With each call I increment pending asynchronous operations counter by one (okay, it is also possible to do before this for loop because Increment() and Decrement() have overloads with count parameter).
HelloWorldCompleted() method is called when asynchronous call is done and data is here. In this method I add new line to result element in AsyncManager property bag. And I decrement the counter because when this method finishes there is one pending asynchronous call fewer then before.
public class HomeController : AsyncController
{
public void IndexAsync()
{
var random = new Random();
for (var i = 0; i < 100; i++)
{
AsyncManager.OutstandingOperations.Increment();
var delay = random.Next(1, 5) * 1000;
var service = new DelayedHelloSoapClient();
service.HelloWorldCompleted += HelloWorldCompleted;
service.HelloWorldAsync(delay);
Debug.WriteLine("Started: " + service.GetHashCode());
}
}
public ActionResult IndexCompleted(List<string> result)
{
return View(result);
}
void HelloWorldCompleted(object sender, HelloWorldCompletedEventArgs e)
{
var hash = 0;
var service = e.UserState as DelayedHelloSoapClient;
if (service != null)
hash = service.GetHashCode();
List<string> list;
if (AsyncManager.Parameters.ContainsKey("result"))
{
list = (List<string>)AsyncManager.Parameters["result"];
}
else
{
list = new List<string>();
AsyncManager.Parameters["result"] = list;
}
list.Add(e.Result);
Debug.WriteLine("Finished: " + hash + " " + e.Result);
AsyncManager.OutstandingOperations.Decrement();
}
public ActionResult About()
{
return View();
}
}
Also notice that I did nothing to bind AsyncManager.Parameters["result"] to result parameter of IndexCompleted() method. This little thing was automated by ASP.NET MVC framework.
Conclusion
ASP.NET MVC supports asynchronous processing very well. It has thinner interface for that than ASP.NET Forms does and we have to write less code. The only not so good thing is incrementing and decrementing pending asynchronous operations counter. But after 15 minutes you don’t forget these two necessary lines anymore. AsyncManager keeps parameters collection that is way better solution than keeping class variables in controller because class variables used by one asynchronous method are may be not used by other methods that need their own variables in class scope. As a conclusion I want to say that writing this sample I found again some brilliant ideas behind ASP.NET MVC framework.
Download and browse source code
View Comments (6)
So does the page render and the user have the ability to do things or does it just sit and spin while you are waiting on background service calls to complete? If it just sits and spins it just seems like a much wiser choice to kick off those lethargic service calls either using AJAX to call directly or wrapping them behind another controller action to map the request.
Nice to hear about you again, Chris! :)
Asynchronous stuff described here is purely server side topic. How you make AJAX requests from client to server is really up to you to decide. It is possible to call web services directly from client side but it is not always option.
There may be web services that you pay for and you cannot show your authentication information to client. Also you may have calls to different services and you want to form one result from answers you got. Third thing is you may want these requests to perform by your web application because this way your clients behind slow connections does not have to wait long.
What is classic synchronous controller? when we implement?
All controllers that are not marked as asynchronous are synchronous. :)
For some reason no one is describing situation, when asynchronous action would throw exception. It's quite difficult to get back Exception data in "Completed" method.
Is there a way to make the same call from client side as a validation check mechanism based on the user's input. For example, validation to check a user's subscription code from a web service without doing a POST to the controller.