ASP.NET MVC Tip #18 – Parameterize the HTTP Context

Context is the enemy of testability. In this tip, I demonstrate how you can eliminate, once and for all, the HTTP Context from an ASP.NET MVC application.

A controller action that interacts only with the set of parameters passed to it is very easy to test. For example, consider the following simple controller action:

VB.NET Version

Public Function InsertCustomer(ByVal firstName As String, ByVal lastName As String, ByVal favoriteColor As String) As ActionResult
    CustomerRepository.CreateCustomer(firstName, lastName, favoriteColor)
 
    Return View()
End Function

C# Version

public ActionResult InsertCustomer(string firstName, string lastName, string favoriteColor)
{
    CustomerRepository.CreateCustomer(firstName, lastName, favoriteColor);          
    
    return View();
}

This controller action creates a new customer. The properties of the new customer are passed as parameters to the action. You can imagine that the source of these parameters is an HTML form. Since the properties are passed as parameters, this controller action is very easy to test.

For example, the following unit tests checks what happens when different unexpected values are passed to the controller action:

VB.NET Version

<TestMethod> _
Public Sub InsertCustomerEmptyValues()
    ' Arrange
    Dim controller As New HomeController()
 
    ' Act
    Dim result As ActionResult = controller.InsertCustomer(String.Empty, String.Empty, String.Empty)
 
    ' Assert
    Assert.IsNotNull(TryCast(result, ViewResult))
End Sub
 
 
 
<TestMethod> _
Public Sub InsertCustomerLongValues()
    ' Arrange
    Dim controller As New HomeController()
 
    ' Act
    Dim longValue As String = "ThisIsAReallyLongValueForThisProperty"
    Dim result As ActionResult = controller.InsertCustomer(longValue, longValue, longValue)
 
    ' Assert
    Assert.IsNotNull(TryCast(result, ViewResult))
End Sub
 

C# Version

[TestMethod]
public void InsertCustomerEmptyValues()
{
    // Arrange
    HomeController controller = new HomeController();
 
    // Act
    ActionResult result = controller.InsertCustomer(String.Empty, String.Empty, String.Empty); 
 
    // Assert
    Assert.IsNotNull(result as ViewResult);
}
 
 
 
[TestMethod]
public void InsertCustomerLongValues()
{
    // Arrange
    HomeController controller = new HomeController();
 
    // Act
    string longValue = "ThisIsAReallyLongValueForThisProperty";
    ActionResult result = controller.InsertCustomer(longValue, longValue, longValue);
 
    // Assert
    Assert.IsNotNull(result as ViewResult);
}

Creating these types of unit tests is easy when everything that the body of an action method touches is passed as a parameter. However, passing parameters is inconvenient when working with large HTML forms. Imagine that a customer has 57 properties. You don’t want to list all 57 properties as parameters to a controller action. Instead, you would create the controller action like this:

VB.NET Version

Public Function InsertCustomer2() As ActionResult
    CustomerRepository.CreateCustomer(Request.Form)
 
    Return View()
End Function

C# Version

public ActionResult InsertCustomer2()
{
    CustomerRepository.CreateCustomer(Request.Form);
 
    return View();
}

In this modified version of the InsertCustomer controller action, the HTML form values are not passed as parameters. Instead, the HTTP Context Request.Form object is used in the body of the controller action to retrieve the HTML form values. This revised InsertCustomer action is easy to write since you don’t need to explicitly list out each and every one of the form fields. Unfortunately, it is much more difficult to test. In order to test this new version of InserCustomer(), you must fake or mock the HTTP Context object.

What we really want to do here is to pass the collection of form values as a single parameter to the InsertCustomer controller action like this:

VB.NET Version

Public Function InsertCustomer3(ByVal formParams As NameValueCollection) As ActionResult
    CustomerRepository.CreateCustomer(formParams)
 
    Return View()
End Function

C# Version

public ActionResult InsertCustomer3(NameValueCollection formParams)
{
    CustomerRepository.CreateCustomer(formParams);
 
    return View();
}

This third version of the InsertCustomer method has all of the advantages, and none of the disadvantages, of the previous two versions. Like the first version of the InsertCustomer action, this third version is easily testable. In a unit test, you simply can create a new NameValueCollection and pass it to the InsertCustomer() method to test the method.

Like the second version of the InsertCustomer action, this third version makes it easy to work with large HTML forms with a large number of input fields. You do not need to explicitly list each of the form parameters. Instead, the parameters are passed as a collection.

In the remainder of this tip, I show you how to implement this third type of controller action by creating a customer Action Invoker and a custom Controller Factory.

Creating a Custom Action Invoker

If we want to modify the parameters passed to a controller action then we need to create something called a custom ControllerActionInvoker. The ControllerActionInvoker is responsible for creating the parameters that are passed to a controller action.

Our custom Action Invoker is contained in Listing 1.

Listing 1 – ContextActionInvoker.vb (VB.NET)

Public Class ContextActionInvoker
    Inherits ControllerActionInvoker
 
    Public Sub New(ByVal controllerContext As ControllerContext)
        MyBase.New(controllerContext)
    End Sub
 
 
    Public Overrides Function InvokeAction(ByVal actionName As String, ByVal values As System.Collections.Generic.IDictionary(Of String, Object)) As Boolean
        Dim context As HttpContextBase = Me.ControllerContext.HttpContext
 
        ' Add Forms Collection
        values.Add("formParams", context.Request.Form)
 
        ' Add User 
        values.Add("isAuthenticated", context.User.Identity.IsAuthenticated)
        values.Add("userName", context.User.Identity.Name)
 
        Return MyBase.InvokeAction(actionName, values)
    End Function
End Class

Listing 1 – ContextActionInvoker.cs (C#)

using System.Web;
using System.Web.Mvc;
 
namespace Tip18.Controllers
{
    public class ContextActionInvoker : ControllerActionInvoker
    {
 
        public ContextActionInvoker(ControllerContext controllerContext):base(controllerContext) {}
 
 
        public override bool InvokeAction(string actionName, System.Collections.Generic.IDictionary<string, object> values)
        {
            HttpContextBase context = this.ControllerContext.HttpContext;
 
            // Add Forms Collection
            values.Add("formParams", context.Request.Form);
            
            // Add User 
            values.Add("isAuthenticated", context.User.Identity.IsAuthenticated);
            values.Add("userName", context.User.Identity.Name);
            
            return base.InvokeAction(actionName, values);
        }
    }
 
}

Our custom action invoker derives from the default ControllerActionInvoker (so we don’t need to start from scratch). We override the InvokeAction() method to modify the way that controller actions are invoked.

Our custom InvokeAction() method sneaks additional values into the Dictionary of values passed to the base InvokeAction() method. We add a value that represents the Request.Form collection, a value that represents whether the current user is authenticated, and a value that represents the current user’s user name.

If a controller action includes a parameter that matches formParams, isAuthenticated, or userName then the parameter will get a value automatically. You can add other magic parameters using this technique as well. For example, you could add a magic roles parameter that always represents the current user's roles.

After you create a custom Action Invoker, you need to create a new Controller Factory so that you can associate your custom Action Invoker with all of your application’s controllers.

Creating a Custom Controller Factory

Our custom Controller Factory is contained in Listing 2. Once again, we do the minimum amount of work by inheriting our custom Controller Factory from the default DefaultControllerFactory class. We simply need to override the GetControllerInstance() method.

Listing 2 – ContextControllerFactory.vb (VB.NET)

Imports System
Imports System.Web.Mvc
 
Public Class ContextControllerFactory
    Inherits DefaultControllerFactory
 
    Protected Overrides Function GetControllerInstance(ByVal controllerType As Type) As IController
        Dim controller As IController = MyBase.GetControllerInstance(controllerType)
        Dim contextController As Controller = TryCast(controller, Controller)
        If contextController IsNot Nothing Then
            Dim context = New ControllerContext(Me.RequestContext, contextController)
            contextController.ActionInvoker = New ContextActionInvoker(context)
        End If
        Return controller
    End Function
 
End Class

Listing 2 – ContextControllerFactory.cs (C#)

using System;
using System.Web.Mvc;
 
namespace Tip18.Controllers
{
    public class ContextControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(Type controllerType)
        {
            IController controller = base.GetControllerInstance(controllerType);
            Controller contextController = controller as Controller;
            if (contextController != null)
            {
                var context = new ControllerContext(this.RequestContext, contextController);
                contextController.ActionInvoker = new ContextActionInvoker(context);
            }
            return controller;
        }
    }
}

Our custom Controller Factory only does one thing. It associates our custom Action Invoker with each controller created by the Controller Factory. Each time the factory creates a new controller, the custom ContextActionInvoker class is assigned to the new controller’s ActionInvoker property.

In order to use a new Controller Factory with an ASP.NET MVC application, you must register the Controller Factory in the Global.asax file. The modified Global.asax file in Listing 3 contains an additional SetControllerFactory() method call in its Application_Start() method.

Listing 3 – Global.asax.vb (VB.NET)

Public Class GlobalApplication
    Inherits System.Web.HttpApplication
 
    Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
 
        ' MapRoute takes the following parameters, in order:
        ' (1) Route name
        ' (2) URL with parameters
        ' (3) Parameter defaults
        routes.MapRoute( _
            "Default", _
            "{controller}/{action}/{id}", _
            New With {.controller = "Home", .action = "Index", .id = ""} _
        )
 
    End Sub
 
    Sub Application_Start()
        ControllerBuilder.Current.SetControllerFactory(GetType(ContextControllerFactory))
        RegisterRoutes(RouteTable.Routes)
    End Sub
End Class

Listing 3 – Global.asax.cs (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Tip18.Controllers;
 
namespace Tip18
{
    public class GlobalApplication : System.Web.HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
            );
 
        }
 
        protected void Application_Start()
        {
            ControllerBuilder.Current.SetControllerFactory(typeof(ContextControllerFactory));
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

Ignore FavIcon Errors when Debugging

When debugging the code for this tip, I kept getting errors related to the FavIcon.ico file. At first, I was confused about the source of these errors.

The FavIcon.ico is a special file that a browser requests from a website. Certain browsers use this icon in the bookmarks/favorites. Certain browsers also display this icon in the title bar, or browser tab, when the website associated with this icon is opened.

When a browser attempts to retrieve the FavIcon.icon file from an ASP.NET MVC Application, the application throws an exception (an ArgumentNullException). The ASP.NET MVC application attempts to map the request to a controller (named FavIcon.ico). Because the controller cannot be found, the ASP.NET MVC framework raises the exception.

You can safely ignore this exception. Alternatively, you can add a FavIcon.ico to the root of your website. The ASP.NET MVC Framework won’t attempt to map a request to a controller action when the file being requested exists in the file system. A third and final option is to create a controller that returns an image for a FavIcon.ico.

To learn more about FavIcon.ico files, see:

http://www.w3.org/2005/10/howto-favicon

Using the Parameterized Context

After you create the custom Action Invoker and custom Controller Factory, the Request.Forms collection and user information is passed to each and every controller action automatically. For example, the modified HomeController in Listing 4 contains two actions that use these magic parameters.

Listing 4 – HomeController.vb (VB.NET)

Public Class HomeController
    Inherits Controller
 
    Public Function InsertCustomer3(ByVal formParams As NameValueCollection) As ActionResult
        CustomerRepository.CreateCustomer(formParams)
        ViewData("FirstName") = formParams("firstName")
        Return View()
    End Function
 
    Public Function TestUserName(ByVal userName As String, ByVal other As String) As ActionResult
        ViewData("UserName") = userName
        Return View()
    End Function
 
End Class
 

Listing 4 – HomeController.cs (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Tip18.Models;
using System.Collections.Specialized;
 
namespace Tip18.Controllers
{
    public class HomeController : Controller
    {
 
        public ActionResult InsertCustomer3(NameValueCollection formParams)
        {
            CustomerRepository.CreateCustomer(formParams);
            ViewData["FirstName"] = formParams["firstName"];
            return View();
        }
 
        public ActionResult TestUserName(string userName, string other)
        {
            ViewData["UserName"] = userName;
            return View();
        }
 
    }
}

The InsertCustomer3() method has a magic parameter named formParams. Because of the name of this parameter, it represents the Request.Form collection automatically.

The TestUserName() method includes two parameters named userName and other. Because the username parameter has the name userName, this parameter gets the current user name automatically. The other parameter is an ordinary parameter. If you include a query string item, form field, or cookie in a browser request named other, then this parameter will have a value.

What happens when you pass a form field or query string named userName to the TestUserName() action? The magic value takes precedence. Therefore, someone can’t spoof the authenticated user name.

Summary

In this tip, I’ve demonstrated how you can kill the HTTP Context once and for all. I’ve demonstrated how you can convert the HTTP Context into a set of parameters passed to each and every controller action. The motivation for making this modification is to create more testable controller action methods.

Clearly, the technique described in this tip could be applied to other types of parameters as well. You can pass any magic values that you want to a controller action simply by creating a new Action Invoker and Controller Factory.

Download the Code

5 Comments

  • Cool stuff! Certainly will use this one!

  • Actually, instead of passing a generic NameValueCollection you should think about passing Data Transfer Objects to your method. While this would mean you would have to write more code, the parameters passed will be strongly typed and named to avoid confusion in your dictionary.

  • Maybe one day the CLR will support the ability to generate data transfer objects with metaprogramming. That way, you get both the dictionary's type affordances and the data transfer object's type assurances.

  • @Rob - You could use a custom ActionInvoker to create a strongly typed Data Transfer Object. However, if you are doing validation on the server-side, then creating a strongly typed object will cause problems (imagine that you have a form field named Price and someone enters "apple" instead of a proper currency amount). If you are doing server-side validation, you really DO NOT want a strongly typed object. Instead, you want a loosely typed collection like the NameValueCollection and you want to pass the collection of inputs to a validation method. (I like the idea of using a DTO when doing all of the validation on the client)

  • Posted too soon. I see from Scott Gutherie's blog that this functionality is going to be part of MVC real soon now.

Comments have been disabled for this content.