Introduction
ASP.NET MVC 2 introduces asynchronous controllers. An asynchronous controller is just a variation of a standard ASP.NET MVC controller that sets up an asynchronous interaction with the surrounding ASP.NET runtime. In a way, asynchronous controllers are the ASP.NET MVC counterpart of asynchronous pages that we have in Web Forms.
Whether you write your site using the ASP.NET MVC framework or the Web Forms framework, any requests that hit your site is processed by the same runtime. Certain resources (i.e., pages or actions) can be configured so that they are processed asynchronously by the ASP.NET runtime. In ASP.NET, asynchronous requests take advantage of asynchronous HTTP handlers, which are a feature of the ASP.NET platform since the first version. However, you don't usually deal with asynchronous HTTP handlers anymore. Both ASP.NET Web Forms and ASP.NET MVC, in fact, provide their own facilities to make it simpler for developers to implement asynchronous actions.
As mentioned, in ASP.NET MVC you have asynchronous controllers. In this article, I'll first briefly describe the mechanics of asynchronous action methods and then move on to discuss how to test asynchronous methods.
What's an Async Controller, Anyway?
Controller actions that run asynchronously are methods of a class that inherits from AsyncController instead of Controller. An async action is actually implemented through a pair of methods following a special naming convention - xxxAsync and xxxCompleted, where xxx indicates the action name. The xxXAsync method is the trigger of the action and can be decorated with built-in and custom action filter attributes. The xxxAsync method performs model binding and triggers the operation that is expected to complete asynchronously. The return value from xxxAsync, if any, is just ignored. The xxxCompleted method receives any value computed during the previous stage and sparks off the creation of the result - be it a HTML view or some other type of action result. Async actions don't need to be bound to special routes and return standard action result objects. Here's a code snippet that illustrates the signature of an asynchronous action named GetNews:
The name of the action is GetNews regardless of the suffix. You can only have one action with a given name whether synchronous or asynchronous. If a conflict is detected, an exception is thrown by the action invoker. The input arguments of the GetNewsAsync method, if any, are resolved through model binding as usual. The method can be decorated with any action filters that you may need. The method internally contains the most of the typical logic of a controller method - processing of input data and performance of the requested action.
What characterizes an async method is that the action is split in two parts that two (potentially distinct) threads take care of. The xxxAsync method starts the asynchronous action and returns. Ideally, the asynchronous action is an action performed outside the process such as invoking an external (Web) service. A good candidate to become an async method is a method that waits for some external response to be generated. The xxxCompleted method just completes the sequence. It grabs any computed data from an internal container and finalizes the request by executing the action result. Here's some code for the two methods introduced earlier.
The xxxAsync method first increases the counter for the pending async operations handled by the instance of the controller. Next, it starts the potentially lengthy, I/O bound operation and sets up a callback for that. The callback, when done, will store arguments for the xxxCompleted method in the Parameters collection of the AsyncManager object and then decrements the counter. The counter is important because when it reaches zero, the invoker executes the xxxCompleted method.
Typical Structure of Async Action Method
In the previous code, all the logic required by the controller methods is placed inline. The controller, therefore, holds dependencies on the network API as well as the async infrastructure. Is this a problem? For sure, this structure doesn't prevent you from writing effective code that just works. But, at the same time, the code is not much flexible and also the level of testability is not really high.
Generally speaking, a good strategy for writing action methods is having them go through three distinct stages:
- Process input data;
- Delegate any significant behavior related with the request to some other service or class;
- Prepare the view model object and execute the results of the previous action;
This pattern greatly simplifies testability because it lets you focus on what the presentation logic in the controller while keeping it separate from any business logic that pertains to the requested action. There might be situations in which once you outsourced the performance of the action to an external service the controller body is pretty thin and even not worthy a test. That's not a situation that should scare you; there's nothing wrong with that. If this is the case, you simply focus on testing the service and let the controller go.
How would deal with the async methods of an ASP.NET MVC controller? Any considerations made thus far about services to which the nitty-gritty business logic is contracted out are still valid for async methods. In this case, though, you may need an additional layer of code sitting in between the controller and the business service just mocking up the asynchrony of the behavior. In other words, your controller method will talk to this intermediate object - known as the Humble Object - always in a synchronous way. The production code of the humble object will then invoke asynchronously any service the business logic requires. While writing tests for the async controller, you only have to mock up the humble object. From the perspective of the test, there's no real difference between a synchronous or asynchronous method.
The "Humble Object" Pattern
The following code shows how to rewrite the previous pair of GetNewsAsync and GetNewsCompleted methods to implement the Humble Object pattern.
As you can see, I use raw dependency injection (also known as the poor man's dependency injection) to let the controller receive from the outside the reference it needs to the humble object. Here's a possible definition for the humble object interface:
And here's its implementation:
The overall behavior of the code doesn't change at all. However, the source code of the controller is much thinner. The humble object is an object that you introduce just because of asynchrony. The Humble Object pattern is a pattern specifically recommended to make asynchronous code more testable. Let's see how to write a test for the controller presented here.
Testing Async Methods
In a test program, you use a test double object to provide a made-to-measure humble object that preserves the behavior of the controller without binding it to the details of the asynchronous service.
The Prepare method maintains exactly the same implementation as the production code. The GetFeedItems method grabs canned data and stores it into the Parameters collection of the AsyncManager object. Finally, it decrements the counter. From the perspective of the controller the interaction with the humble object is always synchronous and in this way it doesn't pose any issue for testability. Here's the code for the test:
At the end of the controller method all that matters is checking whether the Parameters collection is properly filled. The Parameters collection is where the invoker gets the data to pass to the xxxCompleted method. Testing the xxxCompleted method is in no way different from testing a classic synchronous method. More, at least in this case, the code of xxxCompleted is trivial and probably wouldn't even need a test.
Let's Just Wait and Sync Up
By adding an humble object we simply postponed the problem by adding another layer and a deeper separation of concerns. At some point, though, you need to test a service in integration with the rest of the system. In this context, asynchrony can't just be avoided. The only way out is using synchronization primitives. Here's an example based on a controller that doesn't use the humble object. The same approach can be used to write tests against the humble object.
The Finished event on the AsyncManager object notifies ASP.NET MVC that all asynchronous operations are complete and that it is about time the xxxCompleted method is invoked.
Summary
Asynchronous methods in ASP.NET MVC are not a way to improve the user experience but just a way to increase server-side scalability by preserving the threads of the ASP.NET runtime. However, in ASP.NET MVC much more than in ASP.NET Web Forms, asynchronous methods can also be easily combined with AJAX calls to get the best of both worlds: server scalability and user experience. In ASP.NET MVC, in fact, you can use async methods in just any place where you would use a synchronous method including in the <form> tag and hyperlinks. When you do so, you place a request that will be served asynchronously on the server while giving the user a nicer experience through AJAX.
About Dino Esposito
 |
Dino Esposito is one of the world's authorities on Web technology and software architecture. Dino published an array of books, most of which are considered state-of-the-art in their respective areas. His most recent books are “Microsoft ® .NET: Architecting Applications for the Enterprise” and “...
This author has published 53 articles on DotNetSlackers. View other articles or the complete profile here.
|
You might also be interested in the following related blog posts
Html Encoding Nuggets With ASP.NET MVC 2
read more
MvcContrib working on Portable Areas
read more
MvcContrib version control has moved to GitHub
read more
You should NOT use ASP.NET MVC if. . .
read more
MvcContrib v1.0 Released! Download now
read more
MvcContrib Release Candidate posted to CodePlex - now with more consolidated packaging
read more
ASP.NET MVC Release Candidate 2
read more
Donut Caching in ASP.NET MVC
read more
MvcContrib latest release now with SubController support
read more
MvcContrib - now with SubController support for ASP.NET MVC
read more
|
|
Please login to rate or to leave a comment.