现在的位置: 首页 > 综合 > 正文

ASP.NET MVC Tip #18 – 参数化 HTTP Context

2013年06月20日 ⁄ 综合 ⁄ 共 8729字 ⁄ 字号 评论关闭

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

美语原文:http://weblogs.asp.net/stephenwalther/archive/2008/07/11/asp-net-mvc-tip-18-parameterize-the-http-context.aspx
国语翻译:http://www.cnblogs.com/mike108mvp

译者注:在下水平有限,翻译中若有错误或不妥之处,欢迎大家批评指正。谢谢。

译者注:ASP.NET MVC QQ交流群 1215279 欢迎对 ASP.NET MVC 感兴趣的朋友加入

Context是可测试性的敌人。在这篇帖子中,我将演示如何从 ASP.NET MVC application 中一劳永逸地消除HTTP Context。 

一个只与传递给它的参数集进行交互的controller action是非常容易测试的。例如,请看下面的简单controller action代码:

C# Version

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

            
return View();
        }

这个controller action创建了一个新顾客customer。这个customer的各种属性被当作参数传递给action。你可以设想这些参数的来源是一个HTML表单。因为这些属性是以参数形式来传递的,所以非常易于测试。
 

例如,下面的单元测试检测了当意想不到的值被传递给controller action后将发生什么情况。
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);
}

 

当一个action方法函数体能够影响到的所有东东都是以参数传递进来时,那么创建这类的单元测试是很容易的。然而,当与巨大的HTML表单一起工作时,传递这些参数是很不方便的。 设想一下,一个customer有57个属性。你不想在参数中列出这57个属性再传递给一个controller action。取而代之的是,你将创建像这样的controller action:

C# Version

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

            
return View();
        }

 在这个修改过的InsertCustomer controller action中,HTML表单的值并不是作为参数传递的。而是,HTTP Context Request.Form对象在controller action的函数体中被使用,来取得HTML表单的值。这个修订版的InsertCustomer action 是很容易编写的,因为你不必显式列出每个表单域(form fields)。不幸的是,它测试起来就难得多了。为了测试这个新版的InserCustomer(),你必须伪造(fake)或者模拟(mock)出HTTP Context对象。

在这里我们真正想做的是将表单值的集合作为一个单一的参数传递给InsertCustomer controller action,就像这样:

C# Version

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

这个第三版的InsertCustomer方法与前两个版本相比,拥有所有的优势,而没有任何的劣势。就像第一个版本的InsertCustomer action一样,该版本是容易测试的。在一个单元测试中,你可以简单地创建一个新的键值对集合(NameValueCollection)并且将它传递给InsertCustomer()方法来测试它。

就像第二个版本的InsertCustomer action一样,这个第三版本很容易与拥有很多输入域的巨大的HTML表单一起工作。你不必显式列出每个表单参数。取而代之的是,这些参数被当作一个集合来传递。

在这篇帖子的剩余部分,我将展示如何通过创建一个customer Action Invoker和一个custom Controller Factory来实现这个第三种类型的controller action。

创建一个Custom Action Invoker

如果我们想要修改这些传递给controller action的参数,那么我们需要创建一个叫做custom ControllerActionInvoker的东东。这个ControllerActionInvoker负责去创建传递给controller action的参数。

我们的custom Action Invoker包含在代码清单1中。

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<stringobject> 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);
        }

    }


}

 我们的custom action invoker派生于默认的ControllerActionInvoker(因此我们不必从头开始)。我们重写那个InvokeAction() 方法,修改controller actions被调用的方式。

我们的custom InvokeAction()方法偷偷地拿走传递给基类的InvokeAction()方法的值字典(Dictionary of values)中的额外的值。我们添加一个值代表Request.Form集合,该值代表当前用户是否通过了身份验证,该值表示当前用户的用户名(userName)。

如果一个controller action包含一个参数与formParams、isAuthenticated、或者userName匹配,那么该参数将自动得到一个值。你也可以使用这种技术来添加其它魔力参数(magic parameters)。例如你可以添加一个magic roles参数,它始终代表了当前用户的角色(哇Kao,三个戴表啊)。

当你创建一个custom Action Invoker后,你需要创建一个新的Controller Factory以便于你可以将你的custom Action Invoker 与你的应用程序中所有的controllers联系起来。

创建一个Custom Controller Factory

我们的custom Controller Factory包含在代表清单2中。再一次,我们通过从默认的DefaultControllerFactory 类中继承,来用最少的代码实现我们的custom Controller Factory。我们只需要重写GetControllerInstance()方法即可。

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;
        }

    }

}

 我们自定义的Controller Factory只做一件事。它将我们的custom Action Invoker与每个Controller Factory创建的controller联系起来。每次factory创建一个新的controller时,自定义的ContextActionInvoker类就被赋给新的controller的ActionInvoker属性。

为了在ASP.NET MVC application中使用一个新的Controller Factory,你必须在Global.asax文件中注册Controller Factory。修改后的Global.asax文件在代码清单3中,它包含了一个在Application_Start()中添加的SetControllerFactory()方法。

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);
        }

    }

}

调试时忽略FavIcon错误
Ignore FavIcon Errors when Debugging

当调试这篇帖子的代码时,我一直出现一个与FavIcon.ico图标文件相关的错误。起初,我很困惑这些错误的来源。

FavIcon.ico是一个特殊的文件,它是浏览器请求一个网站时出现的。某些浏览器在书签和收藏夹中使用这个图标。在与这些图标相关的网站被打开时,某些浏览器也在标题栏或浏览器标签中中显示这个图标。
 

当一个浏览器试图从一个ASP.NET MVC Application中取得这个FavIcon.icon文件时,应用程序(application)抛出一个异常(ArgumentNullException)。ASP.NET MVC application试图去映射这个请求给一个controller(叫做FavIcon.ico的controller)。因为这个叫做FavIcon.ico的controller找不到,所以ASP.NET MVC框架就出现了这个异常。

你可以安全地忽略这个异常。或者,你可以添加一个FavIcon.ico文件到你的网站根目录中。如果这个被请求的文件存在于文件系统中,则ASP.NET MVC 框架就不会试图将这个请求映射给一个controller action。第三种方式或者说是最终的可选方式是,创建一个controller 来返回一个FavIcon.ico图标。

了解更多有关FavIcon.ico文件信息,请看:

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

使用参数化的Context
 

在你创建一个自定义的Action Invoker和自定义的Controller Factory之后,Request.Forms集合和用户信息就会被自动传递给每个controller action。例如,在代码清单4中修改过的HomeController包含两个使用了这些魔力参数(magic parameters)的action。

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();
        }


    }

}

 InsertCustomer3()方法有一个名为formParams的魔力参数。正如这个参数的名称,它自动代表了Request.Form集合。

TestUserName()方法包含了两个参数,分别是userName和other。正如username参数名叫username,这个参数自动得到当前用户的用户名。other参数是一个普通的参数。如果在一个叫做other的浏览器请求中包含了query string item、或form field、或cookie,那么这个参数将会有一个值。

当你传递一个表单域(form field)或者userName的query string给一个TestUserName()方法时,将会发生什么?该魔力值有优先权。因此,其它人就无法哄骗身份验证通过的用户名。

总结 

在这篇帖子中,作者演示了如何一劳永逸地干掉HTTP Context。也演示了如何将HTTP Context转换为一个参数集,然后传递给每个controller action。做这个修改的动机是创建更易于测试的controller action方法。 

毫无疑问,这篇帖子里描述的技术也可以被用于其它的参数类型。你可以传递任何你想要的魔力参数给controller action,只需简单地创建一个新的Action Invoker和Controller Factory即可。

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.

下载代码:http://weblogs.asp.net/blogs/stephenwalther/Downloads/Tip18/Tip18.zip

 

抱歉!评论已关闭.