Share via



May 2009

Volume 24 Number 05

Extreme ASP.NET - The Life And Times of an ASP.NET MVC Controller

By Scott Allen | May 2009

Code download available

This article is based on a prerelease version of the ASP.NET MVC Framework. Details are subject to change.

Contents

All Routes Lead to a Factory
Factory Extensibility
Execution Is Just the Beginning
Selector Attributes
Filter Attributes
Custom Action Filters
Getting Results
The Action Is Over

Controllers are the lynchpins of the Model View Controller (MVC) design pattern. They are on the front line, first receiving a client's requests, and then translating the requests into instructions for the model where your application's domain logic and data reside. Controllers are also responsible for selecting a view for presenting information to the user.

In this article, I will dissect the ASP.NET MVC framework and look at how controllers work. I'll explain how the framework interacts with your controllers and how you can influence those interactions. I'll look at controller factories, controller actions, action filters, and action results as well.

I'll dig in fairly deeply, so if you are looking for a more general introduction to the ASP.NET MVC framework, see Chris Tavares' article " Building Web Apps without Web Forms."

All Routes Lead to a Factory

It's difficult to talk about the life of a controller without talking about routes. The routing table in an ASP.NET application contains the information necessary for the ASP.NET routing module to extract information from an incoming URL and direct the request to the proper software component. In January's column, I looked at using the ASP.NET routing module with Web Forms (" Routing with ASP.NET Web Forms".) In that column, I built my own routing handler in order to execute a Web form, but the ASP.NET MVC framework provides a routing handler that will eventually direct a request to one of our controllers.

For this MVC routing handler to process requests, you need to configure the routing table during application startup. The default routing configuration provided by the MVC project template lives in the global.asax file and is shown in Figure 1.

Figure 1 The Default Routing Configuration

public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" } ); } protected void Application_Start() { RegisterRoutes(RouteTable.Routes); }

One of the routing configuration entries in Figure 1is a route named Default with an URL template of "{controller}/{action}/{id}". This URL template is a pattern the routing engine will use to first see if an incoming URL is a match for this route. A URL that would match such a route would be https://localhost/home/index/. When the routing engine finds a match, it will again use the URL template as a pattern to lift parameters from the incoming URL. In this example, the string "home" becomes the controller parameter because it is in the {controller} position, and the string "index" becomes the action parameter.

The anonymously typed object constructed as the third parameter to MapRoute represents the default values for the routing engine to use if it cannot find a given parameter in the URL. In the case of https://localhost/home/index/, the routing engine doesn't find "id" information in the URL, but it will still pass along an id parameter with the default value of an empty string. The routing engine passes all these parameters to a routing handler via a RouteData object.

It's important to note that the routing engine knows nothing about ASP.NET MVC. The engine's only job is to analyze URLs and pass control to a route handler. The MapRoute method being invoked inside of the RegisterRoutes method of Figure 1 is an extension method provided by the MVC framework. Every route registered with MapRoute is configured to use an MvcRouteHandler object—this is the route handler provided by the MVC framework. As you saw in the January column, it is the route handler's job to find an HTTP handler for a request—that is an object implementing the IHttpHandler interface. In MVC applications, this object will be an object of type MvcHandler, and inside MvcHandler is where processing becomes interesting.

Figure 2 shows the processing flow for a typical MVC request. When control reaches the MvcHandler, the MvcHandler is able to extract the controller parameter from the RouteData produced by the routing module earlier in the processing. The handler will ultimately send this controller parameter, which is a string value, to a controller factory. It is then the factory's responsibility to construct and return a controller. All controllers in an MVC application implement the IController interface.

fig02.gif

Figure 2 Control Flow for a Typical MVC Request

The MVC framework provides a default controller factory (aptly named DefaultControllerFactory) that will search through all the assemblies in an appdomain looking for all types that implement IController and whose name ends with "Controller." Thus, if you tell the factory to look for a "Home" controller, the factory can return a newly instantiated instance of a HomeController class regardless of the namespace or assembly it lives in—as long as it implements IController. This behavior is part of the MVC's "convention over configuration" style. There is more to the factory story, but let's first finish with the MvcHandler processing.

Once the MvcHandler has an IController reference from the factory, it invokes Execute on the controller and waits for the controller to work its magic. When execution is complete, the MvcHandler will check if the controller implements the IDisposable interface, and if so, will invoke Dispose on the controller to clean up unmanaged resources.

Factory Extensibility

The controller factory is a key extensibility point in the ASP.NET MVC framework. Although the default factory provided by the framework can find a HomeController anywhere in your solution, it can only instantiate the HomeController if you provide a parameterless constructor. This limitation is a problem for teams who follow the dependency inversion principle and inject a controller's dependencies via its constructor. As an example, consider the EmployeeController (shown in Figure 3), which requires someone to pass a logging component into its only constructor.

Figure 3 EmployeeController

public class EmployeeController : IController { public EmployeeController(ILogger logger) { _logger = logger; } public void Execute(RequestContext requestContext) { // ... } ILogger _logger; }

Fortunately, you can create a custom factory. Any class implementing the IControllerFactory interface is a candidate, and you only need to implement CreateController and ReleaseController methods. However, inversion of control containers such as StructureMap, Unity, Ninject, and the Castle project's Windsor are readily available and are a perfect fit for this scenario. In fact, the MVC Contrib project on CodePlexincludes IControllerFactory implementations for all of the containers listed above.

If you want to use StructureMap as your inversion of control container, you can reference the StructureMap and MvcContrib.StructureMap assemblies, then write the code shown in Figure 4. The InitializeContainer method in this listing first tells StructureMap to use a SqlServerLogger type whenever an ILogger is required. The code then sets the controller factory for the entire application using the SetControllerFactory method of the current ControllerBuilder. During request processing, the MvcHandler will ask this same ControllerBuilder for the currently configured factory and work with a StructureMapControllerFactory instead of the default framework factory.

Figure 4 Initialize Container

protected void Application_Start() { InitializeContainer(); RegisterRoutes(RouteTable.Routes); } private void InitializeContainer() { StructureMapConfiguration .ForRequestedType<ILogger>() .TheDefaultIsConcreteType<SqlServerLogger>(); ControllerBuilder.Current.SetControllerFactory( new StructureMapControllerFactory()); }

The StructureMapControllerFactory from the MVC Contrib project does inherit from the MVC framework's default controller factory and will still use the convention I described earlier when locating controller types to instantiate. However, the factory will use StructureMap to instantiate the controller, and StructureMap knows how to work with parameterized constructors. The configuration shown in Figure 4would be everything you need to process a request for https://localhost/Employee/. StructureMap will instantiate the EmployeeController from Figure 3by passing in a SqlServerLogger reference.

fig05.gif

Figure 5 Class Hierarchy

Execution Is Just the Beginning

Earlier I stated that the MvcHandler invokes the controller's Execute method, waits, and then cleans up. This is because the MvcHandler only knows about controllers through an IController interface. If you wanted to write your application at this level, you could simply derive all your controllers from the IController interface and provide an implementation of the Execute method. However, the ASP.NET MVC framework offers a richer execution model for controllers through the hierarchy of classes shown in Figure 5.

By default, the controllers you add to an ASP.NET MVC project derive from the System.Web.Mvc.Controller class. One way to add a new controller is to right-click on the Controllers folder in Solution Explorer and select Add | Controller, which gives you the dialog box shown in Figure 6. Remember, for the controller factory to find your controller the name must end with "Controller."

The Controller base class introduces the concept of an action. Actions are methods on the controller that serve as the ultimate request destination in an MVC application. Earlier, I pointed out that the ASP.NET routing module will lift out "home" as the controller parameter from a URL of https://localhost/home/index/. This is enough information to route the request to the proper controller. The routing module also picked out "index" as an action parameter. When the MvcHandler tells the HomeController to execute, it is up to the logic composed inside the base Controller class to inspect this action parameter and invoke the proper controller method. The majority of this action routing logic resides inside the public ControllerActionInvoker class.

fig06.gif

Figure 6 Add Controller

Figure 7is the HomeController provided by the ASP.NET MVC project template. The public instance methods, Index and About, represent actions the framework will invoke when the client requests home/index/ and home/about/, respectively. Any public instance method can serve as a controller action as long as the framework can determine for certain that the action is the correct action to invoke (in other words, be careful with method overloading). There are additional rules in play when the framework is searching for an action to invoke. You can influence the framework's choice of actions, establish additional rules for action selection, and manipulate the behavior of your actions using attributes.

Figure 7 HomeController

[HandleError] public class HomeController : Controller { public ActionResult Index() { ViewData["Message"] = "Welcome to ASP.NET MVC!"; return View(); } public ActionResult About() { return View(); } }

Selector Attributes

You can decorate a controller action with selector attributes to give the framework additional information to consider when it is selecting an action to invoke. For example, adding a [NonAction] attribute to a controller method will cause the method to be excluded from the list of available actions. You can also give a method a specific action name. By default, the name of a method is also its action name, but if you put [ActionName("help")] on your HomeController's About method, then "about" is no longer a valid action for your controller. Instead, the About method's action name is "help" and the framework will invoke the About method for requests such as /home/help/.

One particularly important selector attribute is the AcceptVerbs attribute. This attribute only allows the framework to select an action when one of the verbs listed in the attribute matches the verb of the current HTTP request. For example, decorating a method with[AcceptVerbs(HttpVerbs.Post)] means that the method can only be invoked as an action for an HTTP POST operation. It is important to select the proper verb for your controller actions, particularly if the actions modify state on the server. For more information, see Stephen Walther's ASP.NET MVC Tip #46.

Filter Attributes

Action filters are another type of attribute you can place on an action. With action filters you can add caching, validation, and error handling behaviors to your actions using a declarative programming model. An example of a filter attribute is the [HandleError] attribute on the HomeController in Figure 7. You can apply this attribute to individual actions, or add the attribute to the controller class to apply the behavior to all actions in a controller.

When a HandleError attribute is present on an action and the action throws an exception, the MVC framework will look for a view with the name of "Error"—first in the controller's view folder, and then also in the shared view folder. The error view allows you to present the user with a friendly error page. You can also map exceptions to specific views using a more explicit HandleError attribute. For example, [HandleError(ExceptionType=typeof(SqlException), View="DatabaseError")] would map an unhandled SqlException to a view named "DatabaseError." The rest of the action filters provided by the MVC framework are outlined in Figure 8.

Figure 8 Action Filters
Name Description
OutputCacheAttribute Similar to the OutputCache directive in ASP.NET Web forms. The OutputCache attribute allows the MVC framework to cache the output of a controller.
ValidateInputAttribute Similar to the ValidateRequest attribute in a Web form. By default, the MVC framework will inspect an incoming HTTP request for HTML or other dangerous input. If detected, an exception will be raised. You can use this attribute to disable the request validation.
AuthorizeAttribute The Authorize attribute allows you to place declarative authorization checks on controller actions. The attribute can restrict an action to users in specific roles. You can use this attribute when you create an action that should only be available to users in an Administrator's role.
ValidateAntiForgeryTokenAttribute This attribute is one half of the solution to help prevent cross-site request forgeries (CSRF). It lets the framework verify presence of a user-specific token in HTTP POSTs. For more on CSRFs, see " Prevent Cross-Site REquest Forgery (CSFR) using ASP.NET MVC's AntiForgeryToken() helper."

Custom Action Filters

You can create your own action filters to surround an action with custom logic. The code in Figure 9is the code for a simple logging action filter that can write to the Output Window of Visual Studio during debugging. We can apply this attribute to an individual action, or we can place this attribute on a controller class to have logging for all the actions in a controller.

Figure 9 A Logging Action Filter

public class LogAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { Log("Action Executing", filterContext.RouteData); } public override void OnActionExecuted(ActionExecutedContext filterContext) { Log("Action Executed", filterContext.RouteData); } public override void OnResultExecuting(ResultExecutingContext filterContext) { Log("Result Executing", filterContext.RouteData); } public override void OnResultExecuted(ResultExecutedContext filterContext) { Log("Result Executed", filterContext.RouteData); } void Log(string stageName, RouteData routeData) { Debug.WriteLine( String.Format("{0}::{1} - {2}", routeData.Values["controller"], routeData.Values["action"], stageName)); } }

As you can see, there are four virtual methods provided by the base ActionFilter class. You can override one or more of these methods to not only pre- and post-process controller actions, but also pre- and post-process the controller action's result. Before an action executes, the OnActionExecuting method will fire, and when the action is complete the OnActionExecuted method will fire (and will fire even if the action throws an unhandled exception). Likewise, the OnResultExecuting method will fire before the result executes and the OnResultExecuted method will fire after.

The context parameter passed into the action filter methods will allow you to inspect the HTTP request, the HTTP context, the route data, and more. Throwing an exception from one of these methods will halt the request processing flow. An exception is a useful tool if you are writing an ActionFilter to check preconditions in the environment.

Getting Results

The successful execution of an MVC controller action will produce an object derived from ActionResult. Rendering a view and redirecting the browser to a new URL are both valid types of results you might want from your controller. The complete list of ActionResult derived types is shown in Figure 10.

Figure 10 ActionResult Derived Types
Name Framework Behavior Producing Method
ContentResult Writes a string value directly into the HTTP response. Content
EmptyResult Does not write to the HTTP response.  
FileContentResult Takes the contents of a file (represented as an array of bytes) and write the contents into the HTTP response. File
FilePathResult Takes the contents of a file at the given location and writes the contents into the HTTP response. File
FileStreamResult Takes a file stream produced by the controller and writes the stream into the HTTP response. File
HttpUnauthorizedResult A special result used by authorization filters when authorization checks fail.  
JavaScriptResult Responds to the client with a script for the client to execute. JavaScript
JsonResult Responds to the client with data in JavaScript Object Notation (JSON). Json
RedirectResult Redirects the client to a new URL. Redirect
RedirectToRouteResult Renders the specified view to respond with an HTML fragment (typically used in AJAX scenarios). RedirectToRoute / RedirectToAction
PartialViewResult Renders the specified view to respond with an HTML fragment (typically used in AJAX scenarios). PartialView
ViewResult Renders the specified view and responds to the client with HTML. View

Note that a controller action never needs to instantiate one of these types directly. Instead, the controller action can invoke the method name shown in Figure 10to produce the result. These methods are inherited from the MVC controller's base class. It's also worth noting that a controller action doesn't need to return an ActionResult object. If the controller returns something other than an ActionResult, the framework will convert the object to a string and wrap the string in a ContentResult (which simply writes the string into the HTTP response). A controller that returns void will produce an EmptyResult.

The ActionResult class defines an ExecuteResult method that each of the types in Figure 10will override. This method is invoked by the ControllerActionInvoker—the same object that called the controller's action. When invoked, each result will take care of all the little details needed to successfully deliver the result to the client. For example, the JavaScript result will set the content type header of the response to "application/x-javascript", while the HttpUnauthorizedResult will set the HTTP status code of the response to a 401 (unauthorized).

A common response from a controller action will be a ViewResult. You've seen this result in previous code listings as all of your actions have invoked the View method of the controller and returned the result. This is another example of "convention over configuration," because the ViewResult will, when used in this default fashion, look for a view in the controller's view folder and with a file name matching the action. For example, views\home\about.aspx is the conventional view for the About action of the Home controller. Overloaded versions of the View method allow you to explicitly state the name of the view.

The Action Is Over

This month I've taken a deep look at the abstractions and behavior surrounding ASP.NET MVC controllers. Yo u should now have a good grasp of how the MVC framework finds, creates, and uses controllers, as well as how to hook into the extensibility points of the MVC framework that revolve around controllers. In the next edition of this column, I'll take a look at guidelines and best practices to put these controllers to work in real applications.

Insights: Helper Methods

If you felt strange using the helper methods for action results when they first appeared in the framework (View(), Content()), you might be wondering how it all came about and what was behind that particular design decision.

Well, the story behind the helper methods for returning action results is that 99% of an MVC developer's time writing an MVC application is going to be spent writing controller actions. We wanted to be sure that the typical method is clean, readable, and as declarative as possible.

For example, you can still write an action method like so:

public ActionResult Foo { // ... do stuff ... ViewData.Model = myModel; return new ViewResult {ViewName = "Foo", ViewData = this.ViewData}; }

We wanted to clean this up a bit, so we made some adjustments, as you can see:

public ActionResult Foo { // ... do stuff ... return new View(myModel); }

This is a more declarative approach (despite the use of a very imperative language). When you read that action method, it reflects your intention. "I want to return a view containing this model."

--Phil Haak, Senior Program Manager, Microsoft

Send your questions and comments to xtrmasp@microsoft.com.

K. Scott Allen is a member of the Pluralsight technical staff and the founder of OdeToCode. You can reach Scott at scott@OdeToCode.comor read his blog at odetocode.com/blogs/scott.