Using an Asynchronous Controller in ASP.NET MVC

The AsyncController class enables you to write asynchronous action methods. You can use asynchronous action methods for long-running, non-CPU bound requests. This avoids blocking the Web server from performing work while the request is being processed. A typical use for the AsyncController class is long-running Web service calls.

This topic contains the following sections:

  • How Requests Are Processed by the Thread Pool

  • Processing Asynchronous Requests

  • Choosing Synchronous or Asynchronous Action Methods

  • Converting Synchronous Action Methods to Asynchronous Action Methods

  • Performing Multiple Operations in Parallel

  • Adding Attributes to Asynchronous Action Methods

  • Working with the BeginMethod/EndMethod Pattern

  • Class Reference

A Visual Studio project with source code is available to accompany this topic: Download.

How Requests Are Processed by the Thread Pool

On the Web server, the .NET Framework maintains a pool of threads that are used to service ASP.NET requests. When a request arrives, a thread from the pool is dispatched to process that request. If the request is processed synchronously, the thread that processes the request is blocked while the request is being processed, and that thread cannot service another request.

This might not be a problem, because the thread pool can be made large enough to accommodate many blocked threads. However, the number of threads in the thread pool is limited. In large applications that process multiple simultaneous long-running requests, all available threads might be blocked. This condition is known as thread starvation. When this condition is reached, the Web server queues requests. If the request queue becomes full, the Web server rejects requests with an HTTP 503 status (Server Too Busy).

Processing Asynchronous Requests

In applications where thread starvation might occur, you can configure actions to be processed asynchronously. An asynchronous request takes the same amount of time to process as a synchronous request. For example, if a request makes a network call that requires two seconds to complete, the request takes two seconds whether it is performed synchronously or asynchronously. However, during an asynchronous call, the server is not blocked from responding to other requests while it waits for the first request to complete. Therefore, asynchronous requests prevent request queuing when there are many requests that invoke long-running operations.

When an asynchronous action is invoked, the following steps occur:

  1. The Web server gets a thread from the thread pool (the worker thread) and schedules it to handle an incoming request. This worker thread initiates an asynchronous operation.

  2. The worker thread is returned to the thread pool to service another Web request.

  3. When the asynchronous operation is complete, it notifies ASP.NET.

  4. The Web server gets a worker thread from the thread pool (which might be a different thread from the thread that started the asynchronous operation) to process the remainder of the request, including rendering the response.

The following illustration shows the asynchronous pattern.

Async pipeline

Choosing Synchronous or Asynchronous Action Methods

This section lists guidelines for when to use synchronous or asynchronous action methods. These are just guidelines; you must examine each application individually to determine whether asynchronous action methods help with performance.

In general, use synchronous pipelines when the following conditions are true:

  • The operations are simple or short-running.

  • Simplicity is more important than efficiency.

  • The operations are primarily CPU operations instead of operations that involve extensive disk or network overhead. Using asynchronous action methods on CPU-bound operations provides no benefits and results in more overhead.

In general, use asynchronous pipelines when the following conditions are true:

  • The operations are network-bound or I/O-bound instead of CPU-bound.

  • Testing shows that the blocking operations are a bottleneck in site performance and that IIS can service more requests by using asynchronous action methods for these blocking calls.

  • Parallelism is more important than simplicity of code.

  • You want to provide a mechanism that lets users cancel a long-running request.

The downloadable sample shows how to use asynchronous action methods effectively. The sample program calls the Sleep method to simulate a long-running process. Few production applications will show such obvious benefits to using asynchronous action methods.

You should test applications to determine whether asynchronous methods provide a performance benefit. In some cases it might be better to increase the IIS maximum concurrent requests per CPU and the maximum concurrent threads per CPU. For more information about ASP.NET thread configuration, see the entry ASP.NET Thread Usage on IIS 7.0 and 6.0on Thomas Marquardt's blog. For more information about when to make asynchronous database calls, see the entry Should my database calls be Asynchronous? on Rick Anderson's blog.

Few applications require all action methods to be asynchronous. Often, converting a few synchronous action methods to asynchronous methods provides the best efficiency increase for the amount of work required.

Converting Synchronous Action Methods to Asynchronous Action Methods

The sample code below shows a synchronous action method that is used to display news items from a portal controller. The request Portal/News?city=Seattle displays news for Seattle.

public class PortalController: Controller {
    public ActionResult News(string city) {
        NewsService newsService = new NewsService();
        ViewStringModel headlines = newsService.GetHeadlines(city);
        return View(headlines);
    }
}

The following example shows the News action method rewritten as an asynchronous method.

public class PortalController : AsyncController {
    public void NewsAsync(string city) {

        AsyncManager.OutstandingOperations.Increment();
        NewsService newsService = new NewsService();
        newsService.GetHeadlinesCompleted += (sender, e) =>
        {
            AsyncManager.Parameters["headlines"] = e.Value;
            AsyncManager.OutstandingOperations.Decrement();
        };
        newsService.GetHeadlinesAsync(city);
    }

    public ActionResult NewsCompleted(string[] headlines) {
        return View("News", new ViewStringModel
        {
            NewsHeadlines = headlines
        });
    }
}
Public Class PortalController
    Inherits AsyncController
    
    Public Sub NewsAsync(ByVal city As String) 
        AsyncManager.OutstandingOperations.Increment() 
        Dim newsService As New NewsService() 
        newsService.GetHeadlinesCompleted += Function(sender, e) Do 
        AsyncManager.Parameters("headlines") = e.Value 
        AsyncManager.OutstandingOperations.Decrement() 
    End Function 
    newsService.GetHeadlinesAsync(city) 
End Sub
    
    Public Function NewsCompleted(ByVal headlines() As String) _
            As ActionResult
        Return View("News", New ViewStringModel() {NewsHeadlines=headlines})
    End Function
End Class

To convert a synchronous action method to an asynchronous action method involves the following steps:

  1. Instead of deriving the controller from Controller, derive it from AsyncController. Controllers that derive from AsyncController enable ASP.NET to process asynchronous requests, and they can still service synchronous action methods.

  2. Create two methods for the action. The method that initiates the asynchronous process must have a name that consists of the action and the suffix "Async". The method that is invoked when the asynchronous process finishes (the callback method) must have a name that consists of the action and the suffix "Completed". In the previous example, the News method has been turned into two methods: NewsAsync and NewsCompleted.

    The NewsAsync method returns void (no value in Visual Basic). The NewsCompleted method returns an ActionResult instance. Although the action consists of two methods, it is accessed using the same URL as for a synchronous action method (for example, Portal/News?city=Seattle). Methods such as RedirectToAction and RenderAction will also refer to the action method as News and not NewsAsync.

    The parameters that are passed to NewsAsync use the normal parameter binding mechanisms. The parameters that are passed to NewsCompleted use the Parameters dictionary.

  3. Replace the synchronous call in the original ActionResult method with an asynchronous call in the asynchronous action method. In the example above, the call to newsService.GetHeadlines is replaced with a call to newsService.GetHeadlinesAsync.

The NewsService class that is consumed by the NewsAsync method is an example of a service that exposes methods using an event-based asynchronous pattern. For more information about this pattern, see Event-based Asynchronous Pattern Overview.

The OutstandingOperations property notifies ASP.NET about how many operations are pending. This is required because ASP.NET cannot determine how many operations were initiated by the action method or when those operations are complete. When OutstandingOperations property is zero, ASP.NET completes the overall asynchronous operation by calling the NewsCompleted method.

Note the following about asynchronous action methods:

  • If the action name is Sample, the framework will look for SampleAsync and SampleCompleted methods.

  • View pages should be named Sample.aspx rather than SampleAsync.aspx or SampleCompleted.aspx. (The action name is Sample, not SampleAsync.)

  • A controller cannot contain an asynchronous method named SampleAsync and a synchronous method named Sample. If it does, an AmbiguousMatchException exception is thrown because the SampleAsync action method and the Sample action method have the same request signature.

Performing Multiple Operations in Parallel

Asynchronous action methods are useful when an action must perform several independent operations. For example, a portal site might show not only news, but sports, weather, stocks, and other information.

The following example shows a synchronous version of news portal Index action method.

public ActionResult IndexSynchronous( string city ) {
    
    NewsService newsService = new NewsService();
    string[] headlines = newsService.GetHeadlines();

    SportsService sportsService = new SportsService();
    string[] scores = sportsService.GetScores();

    WeatherService weatherService = new WeatherService();
    string[] forecast = weatherService.GetForecast();

    return View("Common", new PortalViewModel  {
        NewsHeadlines = headlines,
        SportsScores = scores,
        Weather = forecast
    });
}
Public Function IndexSynchronous(ByVal city As String) As ActionResult
    Dim newsService As NewsService = New NewsService
    Dim headlines() As String = newsService.GetHeadlines
    Dim sportsService As SportsService = New SportsService
    Dim scores() As String = sportsService.GetScores
    Dim weatherService As WeatherService = New WeatherService
    Dim forecast() As String = weatherService.GetForecast
    Return View("Common", New PortalViewModel() {NewsHeadlines=headlines, SportsScores=scores, Weather=forecast})
End Function

The calls to each service are made sequentially. Therefore, the time that is required in order to respond to the request is the sum of each service call plus a small amount of overhead. For example, if the calls take 400, 500, and 600 milliseconds, the total response time will be slightly more than 1.5 seconds. However, if the service calls are made asynchronously (in parallel), the total response time will be slightly more than 600 milliseconds, because that is the duration of the longest task.

The following example shows an asynchronous version of the news portal Index action method.

public void IndexAsync(string city) {
    AsyncManager.OutstandingOperations.Increment(3);

    NewsService newsService = new NewsService();
    newsService.GetHeadlinesCompleted += (sender, e) =>
    {
        AsyncManager.Parameters["headlines"] = e.Value;
        AsyncManager.OutstandingOperations.Decrement();
    };
    newsService.GetHeadlinesAsync();

    SportsService sportsService = new SportsService();
    sportsService.GetScoresCompleted += (sender, e) =>
    {
        AsyncManager.Parameters["scores"] = e.Value;
        AsyncManager.OutstandingOperations.Decrement();
    };
    sportsService.GetScoresAsync();

    WeatherService weatherService = new WeatherService();
    weatherService.GetForecastCompleted += (sender, e) =>
    {
        AsyncManager.Parameters["forecast"] = e.Value;
        AsyncManager.OutstandingOperations.Decrement();
    };
    weatherService.GetForecastAsync();
}

public ActionResult IndexCompleted(string[] headlines, string[] scores, string[] forecast) {
    return View("Common", new PortalViewModel  {
        NewsHeadlines = headlines,
        SportsScores = scores,
        Weather = forecast
    });
}          
}
Public Sub IndexAsync(ByVal city As String)
    AsyncManager.OutstandingOperations.Increment(3)
    Dim newsService As New NewsService() 
    newsService.GetHeadlinesCompleted += Function(sender, e) Do 
        AsyncManager.Parameters("headlines") = e.Value 
        AsyncManager.OutstandingOperations.Decrement() 
    End Function 
    newsService.GetHeadlinesAsync() 
    
    Dim sportsService As New SportsService() 
    sportsService.GetScoresCompleted += Function(sender, e) Do 
        AsyncManager.Parameters("scores") = e.Value 
        AsyncManager.OutstandingOperations.Decrement() 
    End Function 
    sportsService.GetScoresAsync() 
    
    Dim weatherService As New WeatherService() 
    weatherService.GetForecastCompleted += Function(sender, e) Do 
        AsyncManager.Parameters("forecast") = e.Value 
        AsyncManager.OutstandingOperations.Decrement() 
    End Function 
    weatherService.GetForecastAsync()
End Sub

Public Function IndexCompleted(ByVal headlines() As String, _
        ByVal scores() As String, _
        ByVal forecast() As String) As ActionResult
    Return View("Common", New PortalViewModel() {NewsHeadlines=headlines, SportsScores=scores, Weather=forecast})
End Function

In the previous example, the Increment method is called with a parameter of 3 because there are three asynchronous operations.

Adding Attributes to Asynchronous Action Methods

If you want to apply attributes to an asynchronous action method, apply them to the ActionAsync method instead of to the ActionCompleted method. Attributes on the ActionCompleted method are ignored.

Two new attributes have been added: AsyncTimeoutAttribute and NoAsyncTimeoutAttribute. These attributes let you control the asynchronous timeout period.

Working with the BeginMethod/EndMethod Pattern

If an asynchronous action method calls a service that exposes methods by using the BeginMethod/EndMethod pattern, the callback method (that is, the method that is passed as the asynchronous callback parameter to the Begin method) might execute on a thread that is not under the control of ASP.NET. In that case, HttpContext.Current will be null, and the application might experience race conditions when it accesses members of the AsyncManager class such as Parameters. To make sure that you have access to the HttpContext.Current instance and to avoid the race condition, you can restore HttpContext.Current by calling Sync() from the callback method.

If the callback completes synchronously, the callback will be executed on a thread that is under the control of ASP.NET and the operations will be serialized so there are no concurrency issues. Calling Sync() from a thread that is already under the control of ASP.NET has undefined behavior.

The ActionCompleted method will always be called on a thread that is under the control of ASP.NET. Therefore, do not call fSync() from that method.

The callback that you pass to the Begin method might be called using a thread that is under the control of ASP.NET. Therefore, you must check for this condition before you call Sync(). If the operation completed synchronously (that is, if CompletedSynchronously is true), the callback is executing on the original thread and you do not have to call Sync(). If the operation completed asynchronously (that is, CompletedSynchronously is false), the callback is executing on a thread pool or I/O completion port thread and you must call Sync().

For more information about the BeginMethod/EndMethod pattern, see Asynchronous Programming Overview and the entry Using the BeginMethod/EndMethod pattern with MVC on Rick Anderson's blog.

Class Reference

The following table lists the key classes for asynchronous action methods.

Class

Description

AsyncController

Provides the base class for asynchronous controllers.

AsyncManager

Provides asynchronous operations for the AsyncController class.

See Also

Concepts

Controllers and Action Methods in ASP.NET MVC Applications