《Pro ASP.NET MVC 3 Framework》学习笔记之二十六【Controller扩展】-attach

本章内容分为两个部分,第一部分:介绍关于controllers工作原理的高级功能,探究从请求到action方法执行的整个请求处理管道的组成部分并阐释控制这个过程的不同的方式;第二部分:介绍两种特殊的控制器,分别是:无会话(sessionless)控制器,异步(asynchronous)控制器.这些能够增进服务器的处理能力。这部分会阐释如何创建和使用它们,并且会说明在什么情况下使用它们。

请求处理管道的组成(Request Processing Pipeline Components)


我们的焦点放在第一部分的Controller Factory和Action Invoker上面,其实从命名上看已经能够了解它们的用途了。具体如下:
控制器工厂(Controller Factory):创建一个用来服务请求的控制器的实例。
Action调用(Action Invoker):找到并调用在Controller类里面的Action方法。
MVC框架包含了对这些组件的默认实现,下面会进行介绍。

创建一个自定义的控制器工厂(Defining a Custom Controller Factory)

Controller工厂是定义在IControllerFactory接口里面的,如下:

View Code

using System.Web.Routing; 
using System.Web.SessionState;

namespace System.Web.Mvc
{
public interface IControllerFactory
{

IController CreateController(RequestContext requestContext, string controllerName);
SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext,
string controllerName);
void ReleaseController(IController controller);
}
}

这个接口里面最重要的方法是CreateController,当需要一个控制器来处理请求的时候,MVC框架会调用它来创建一个控制器的实例。

其实不推荐创建自定义的控制器,其中的一个原因是:寻找在Web应用程序里面的控制器类并实例化它们是非常复杂的。下面是例子是一个简单的演示,该控制器工厂仅仅支持两个控制器FirstController和SecondController.如下所示:

View Code

//这部分是CustomControllerFactory代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using ControllerExtensibility.Controllers;

namespace ControllerExtensibility.Infrastructure
{
public class CustomControllerFactory : IControllerFactory
{
public IController CreateController(RequestContext requestContext, string controllerName)
{
Type targetType = null;
switch (controllerName)
{
case "Home":
requestContext.RouteData.Values["controller"] = "First";
targetType = typeof(FirstController);
break;
case "First":
targetType = typeof(FirstController);
break;
case "Second":
targetType = typeof(SecondController);
break;
default:
break;
}
return targetType == null ? null : (IController)Activator.CreateInstance(targetType);
}

public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{
return SessionStateBehavior.Default;
}

public void ReleaseController(IController controller)
{
IDisposable disposable = controller as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
}
}

//FirstController代码
namespace ControllerExtensibility.Controllers
{
public class FirstController : Controller
{
//
// GET: /First/

public ActionResult Index()
{
ViewBag.Message = "Hello from the FirstController class";
return View();
}

}
}

//SecondController代码
namespace ControllerExtensibility.Controllers
{
public class SecondController : Controller
{
//
// GET: /Second/

public ActionResult Index()
{
ViewBag.Message = "Hello from SecondController class";
return View();
}

}
}

//两个Controller对应的View代码如下:
@{
ViewBag.Title = "Index";
}


This is the second controller view


Message:@ViewBag.Message

@{
ViewBag.Title = "Index";
}

This is the first controller view


Message:@ViewBag.Message

CreateController方法的目的是创建一个能够处理请求的控制器实例,工厂如何处理的过程是完全开放的。到目前为止,我们从书里面的例子看到的约定是存在的,因为这种方式正是默认控制器的工作方式,在我们完成了自定义工厂以后会覆盖默认的工厂,在我们上面的例子中,忽略了所有的约定,而是实现我们自己的逻辑。这是非常奇怪的事情,但是也能反映MVC框架提供的完全的灵活性。

如果我们收到一个请求,controller的值是First或者Second,我们创建FirstController类或者SecondController类的实例。创建实例使用的是System.Activator类,它通过targetType的类型创建一个对象的实例,然后转换为IController类型。例如:(IController)Activator.CreateInstance(targetType); 当我们收到一个controller值是Home的请求,我们也将它映射到了FirstController类,当然这也非常奇怪的,却能够说明请求和controller之间的映射仅仅是控制器工厂(controller factory)的职责,但是我们不能说映射请求到视图。

MVC框架是根据routing data里面controller的值来选择视图的。例如,如果我们想映射一个对Home控制器的请求到First控制器的实例,我们需要改变请求里面controller的值。像这样:requestContext.RouteData.Values[“controller”] = “First”;

所以,控制器工厂不仅有匹配请求到控制器的职责,而且能够改变请求并改变请求处理管道后续步骤的行为。这是一个强有力的而且是MVC框架关键性的组件。下面介绍在IControllerFactory接口里面的另外两个方法:

a.GetControllerSessionBehavior:被MVC用来决定是否应该为控制器维护一个会话数据。
b.ReleaseController:当一个控制器对象不在被需要的时候调用该方法。在我们实现里面是检查这个类是否实现IDisposable接口,如果实现了,就可以调用Dispose方法来释放不在使用的资源。

注册自定义的控制器工厂(Registering a Custom Controller Factory)

我们通过ControllerBuilder类告诉MVC框架使用自己的控制器工厂。如下所示:

View Code

protected void Application_Start() 
{
AreaRegistration.RegisterAllAreas();

ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());

RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}

使用内置控制器工厂(Working with the Built-In Controller Factory)

对大多数应用程序,使用内置的控制器工厂类DefaultControllerFactory已经足够了。当它接收到一个来自路由系统的请求时,这个工厂会在routing data里面寻找controller属性的值,并试图在web应用程序里面找到一个满足下面条件的类:

a.这个类必须是Public   b.这个类是能够实例化对象的(不是抽象的)  c.这个类不能带泛型参数  d.类的名字必须以Controller结尾  e.必须实现IController接口

DefaultControllerFactory类维护着这些类的一个列表,以致于它不用在每一次请求到达时都去执行对每一个类的查找。如果找到了适合的类就会使用控制器激活器(controller activator)创建一个实例,这时控制器的工作就完成了。如果没有匹配的控制器,那么这个请求就不能被进一步处理。

注意一下DefaultControllerFactory是如何遵循”约定胜于配置”模式的——你不需要在配置文件里面注册控制器,因为这个工厂会为我们找到他们。我们只要创建一个满足工厂寻找条件的类就行了。

如果我们想创建自定义控制器工厂的行为,可以配置默认工厂的设置或重写一些方法。通过这种方式,能够创建有用的符合”约定胜于配置”的行为并且不需要重复创建。下面会展示裁剪控制器创建的不同方式。

指定优先的命名空间(Prioritizing Namespaces)

在前面的章节里面我们知道了如何在创建路由的时候指定一个或多个优先的命名空间。这是为了解决具有相同名字并且不在同一命名空间下的控制器模棱两可的问题。其实在前面章节用到指定优先的命名空间实际上是DefaultControllerFactory处理的。如果我们的应用程序有很多的路由,指定全局的优先命名空间是非常方便的,这样可以应用所有的路由。如下所示:

View Code

protected void Application_Start() 
{
AreaRegistration.RegisterAllAreas(); ControllerBuilder.Current.DefaultNamespaces.Add("MyControllerNamespace");
ControllerBuilder.Current.DefaultNamespaces.Add("MyProject.*");

RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}

可见,我们是用静态的ControllerBuilder.Current.DefaultNamespaces.Add方法添加优先的命名空间。我们添加的顺序并不代表任何对命名空间的查找顺序,所有的默认命名空间将被用来搜寻候选的Controller类,并且当我们直接执行在路由里面定义的相同的任务时,会因为重复引发异常。

Tips:全局指定的优先命名空间会被我们在具体定义路由时指定的优先命名空间所覆盖。
如果控制器工厂不能在我们指定的命名空间里面找到适合的控制器,会接着去匹配其他的。上面指定时有一个使用”*”,这个表示匹配以MyProject开头的所有子命名空间,注意这里的”*”并不是正则表达式,而且这里也不能使用除”*”以外的正则符号。

定制DefaultControllerFactory控制器的创建方式

有很多种方式可以定制DefaultControllerFactory类创建控制器对象,这样做的一个最常见的理由是添加对DI(依赖注入)的支持。可以采取很多种方式来这样做,最合适的方式取决于在应用程序的其他的地方如何使用DI。可以使用依赖解析器创建控制器,这个在最开始的几章里面有介绍过使用NinjectDependencyResolver。

还有一种方式:使用控制器激活器。如下所示:

View Code

using System.Web.Routing; 

namespace System.Web.Mvc
{
public interface IControllerActivator
{
IController Create(RequestContext requestContext, Type controllerType);
}
}

实例化上面的接口,如下:

View Code

namespace ControllerExtensibility.Infrastructure
{
public class CustomControllerActivator : IControllerActivator
{
public IController Create(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
if (controllerType == typeof(FirstController))
{
controllerType = typeof(SecondController);
}
return DependencyResolver.Current.GetService(controllerType) as IController;
}
}
}

我们的实现是传递请求给了依赖解析器,当然除非它们是FirstController类型。在那种情下,我们请求一个SecondController类的实例。

IControllerActivator接口仅仅在我们也使用了依赖解析器时被使用,这是因为DefaultControllerFactory类通过调用IDependencyResolver.GetService方法来寻找一个控制器激活器。所以,我们需要使用依赖解析器直接注册激活器,正如使用NinjectDependencyResolver类的AddBindings方法。注册一个控制器激活器的示例如下:

View Code

private void AddBindings() 
{
// put bindings here
Bind().To();
}

依靠依赖解析器创建控制器是非常简单的,然而如果我们想拦截和操控请求,那么使用控制器激活器就是一个非常有用并且适合的功能。

重写DefaultControllerFactory方法(Overriding DefaultControllerFactory Methods)

可以通过重写DefaultControllerFactory类的方法来定制控制器的创建,能够重写的方法有:
CreateController(返回IController类型):是IControllerFactory接口CreateController方法的实现,默认情况下,这个方法调用GetControllerType决定哪一种类型应该被实例化,然后通过传递GetControllerInstance方法的结果获取一个控制器对象。
GetControllerType(返回Type类型):映射请求到控制器的类型
GetControllerInstance(返回IController类型):创建一个具体类型的实例

创建一个自定义的Action调用者(Creating a Custom Action Invoker)

一旦控制器工厂创建了一个类的实例,MVC框架需要一种能够调用这个实例上Action的方式。如果我们的控制器是从Controller类派生的,那么调用Action的是action调用者的职责。如果是通过直接实现IController接口来创建控制器,那么我们就直接负责调用action了,下面是IActionInvoker接口的代码:

View Code

namespace System.Web.Mvc 
{
public interface IActionInvoker
{
bool InvokeAction(ControllerContext controllerContext, string actionName);
}
}

接口仅有一个成员:InvokerAction。返回值是bool,true表示action已经找到并且被调用,false表示controller没有匹配的action。注意这里的描述并没有使用method方法,而是使用的action,因为action跟method之间的关联是严格可选的。下面是一个自定义的Action调用者:

View Code

namespace ControllerExtensibility.Infrastructure
{
public class CustomActionInvoker : IActionInvoker
{
public bool InvokeAction(ControllerContext controllerContext, string actionName)
{
if (actionName == "Index")
{
controllerContext.HttpContext.Response.Write("This is output from the Index action");
return true;
}
else
{
return false;
}
}
}
}

Action调用者不关心controller类里面的方法,实际上,它只处理action本身。如果请求是针对Index Action的,那么调用者就会写一条信息到Response。如果不是则返回false,这样会造成404错误展示给用户。关联一个控制器的action调用者是通过Controller.ActionInvoker属性获得的。这意味着在同一个应用程序里面的不同控制器能够使用不同的action调用者。下面展示了一个使用action调用者的例子:

View Code

using ControllerExtensibility.Infrastructure;

namespace ControllerExtensibility.Controllers
{
public class CustomActionInvokerController : Controller
{
public CustomActionInvokerController()
{
ActionInvoker = new CustomActionInvoker();
}
}
}

在上面这个controller里面没有action方法,它依靠action调用者来处理请求。不建议实现自己的action调用者,因为内置的支持有非常有用的功能。

使用内置的Action调用者(Using the Built-In Action Invoker)

ControllerActionInvoker类是内置的action调用者,具有匹配请求到action的非常复杂精细的技术。不像我们在前面的实现,默认的action调用者在方法上操作,能够作为action的方法必须满足一下条件:(ps:到这里应该明白为什么前面会说action和method在MVC里表述是不同的吧,呵呵)

a.方法必须是Public   b.方法不能是static的   c.方法不能是System.Web.Mvc.Controller类或它的基类里面已经存在的  d.方法名不能特殊

前面两个条件很容易理解,第三个条件是指像ToString()和GetHashCode()要排除,因为这些是实现了IController接口的,这是容易意识到的,因为我们不想暴露控制器的内部运行原理。最后一个条件的意思是构造器,属性,事件访问器要排除。实际上,没有来自 System.Reflection.MethodBase类并具有IsSpecialName标志的类成员会被用来处理一个action。

注:具有泛型参数的方法满足上面四个条件,但是如果我们视图调用这样的一个方法来处理请求,MVC框架会抛异常。
默认情况下,ControllerActionInvoker会寻找具有与被请求的action相同的名字。

例如,如果路由系统产生的action的值是Index,那么ControllerActionInvoker会寻找名为Index并满足四个条件的方法。如果找到了这样一个方法,就会调用它来处理请求。

使用自定义的Action命名(Using a Custom Action Name)

通常情况下,action方法的名字决定了它说表现的action,Index action方法为针对Index action的请求服务。我们能够通过ActionName属性重写这种行为,如下所示:

View Code

using System.Web.Mvc;

namespace ActionInvokers.Controllers
{
public class HomeController : Controller
{
[ActionName("Index")]
public ActionResult MyAction()
{
return View();
}

}
}

上面的例子应用了ActionName这个特性将MyAction重写为Index,这意味着MyAction方法不在用来针对MyAction的action进行服务,而是对Index的action进行服务。要重写ActionName一般会是以下两个原因:

a.接收一个不合法的C#方法名。如[ActionName(“User-Registration”)]
b.如果想拥有两个不同的C#方法接收同一套参数并处理同样的action名,但是为了响应不同的HTTP请求类型(HttpGet/HttpPost),我们能够给定方法不同的名字来满足编译,但是使用[ActionName]映射两个相同的action名。

一个比较奇怪的地方是,当我们在MyAction上右键创建视图时,弹出的对话框依然默认显示的是MyAction,如图:

使用Action方法选择(Using Action Method Selection)

一个控制器包含多个同名的action是非常常见的情形,因为不同的参数或者使用【ActionName】,所以导致多个方法表示同样的action.这种情形下,MVC框架需要一些辅助选择合适的action来处理请求,把这种机制成为:action方法选择(action method selection).它允许我们定义一个action将要处理的多种请求。其实这个在最开始的那个SportsStore项目里面已经有了展示。对action方法设置【HttpPost】或【HttpGet】,让同名的action处理不同的请求。如:

View Code

... 
[HttpPost]
public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails) {
// 方法主体
}

public ViewResult Checkout() {
// 方法主体
}
...

action调用者使用一个action方法选择器消除了在选择一个action时模棱两可的情况。还有一个【NonAction】特性表示该方法永远不会被当中action使用,这个特性可以防止我们暴露controller类的工作原理。当然,这些方法应该是private的。当然如果方法是Public而又不想被当作action使用,就可以使用【NonAction】特性来实现。

创建自定义的Action方法选择器(Creating a Custom Action Method Selector)

action方法选择器是从ActionMethodSelectorAttribute类派生的,如下所示:

View Code

using System; 
using System.Reflection;

namespace System.Web.Mvc
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public abstract class ActionMethodSelectorAttribute : Attribute {
public abstract bool IsValidForRequest(ControllerContext controllerContext,
MethodInfo methodInfo);
}
}

ActionMethodSelectorAttribute是抽象的并定义了一个抽象方法:IsValidForRequest。参数ControllerContext让我们获取请求的信息,MethodInfo对象获取应用了选择器的方法的信息。如果方法能够处理请求则返回true,否则返回false。下面是一个自定义的action方法选择器:

View Code

using System.Web.Mvc;
using System.Reflection;

namespace ActionInvokers.Infrastructure
{
public class LocalAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
return controllerContext.HttpContext.Request.IsLocal;
}
}
}

这个选择器的作用是当请求源是本地机器时返回true。可以这样使用它,如下:

View Code

... 
[ActionName("Index")]
public ActionResult FirstMethod()
{
return View((object)"Message from FirstMethod");
}

[Local]
[ActionName("Index")]
public ActionResult SecondMethod()
{
return View((object)"Message from SecondMethod");
}
...

当controller收到一个针对Index action的请求,Local选择器会进行评估,如果满足条件(请求源是本地机器),那么第二个action方法会被调用,否则调用第一个。
不建议使用action选择器作为一种安全检查,除非我们想调试。

Action方法选择器消除歧义的处理过程

Action调用者是从一个满足action四个条件的controller里面的可能的方法列表开始处理过程,步骤如下:

a.调用者根据名字会丢弃不满足的方法
b.调用者会丢弃任何具有action方法选择器的特性(attribute)(该特性对当前请求返回false)的方法
c.如果刚好有一个action保留下来,那么这个方法会被使用。如果有多个保留下来,会抛出异常,因为这样没有消除歧义。
d.如果一个方法都没有保留下来,那么调用者会继续进行寻找不使用选择器的方法。如果刚好有一个保留下来,那么直接使用,如果是多个,则抛异常。

处理未知的Action(Handling Unknown Actions)

如果action调用者不能找到一个action方法调用,它会返回false。发生这种情况时,Controller类会调用HandleUnknownAction方法,默认情况下,这个方法返回404-NOT Found到客户端。这是一个controller能够做的最有意义的事情了,当然我们能够选择重写controller类里面的这个方法来做些比较特别的事情。下面是一个重写的例子:

View Code

using System.Web.Mvc;

namespace ActionInvokers.Controllers
{
public class HomeController : Controller
{
[ActionName("Index")]
public ActionResult MyAction()
{
return View();
}

protected override void HandleUnknownAction(string actionName)
{
Response.Write(string.Format("You request the {0} action", actionName));
}
}
}

使用Action方法选择器支持REST服务(Using Action Method Selectors to Support REST Services)

最近这些年,越来越多的开发者选择使用REST方式代替SOAP实现Web服务。通过联合一个URL和HTTP方法来指定一个REST操作,URL具有应用程序的具体含义。例如有这样一个URL:/Staff/1,指向在staff数据库的第一条记录。不同的HTTP方法代表在这个记录上执行的不同操作。Get请求检索数据,Post请求修改数据,DELETE删除数据等等。我们可以通过联合ActionName特性和action方法选择器提供REST支持。支持REST Web服务:

View Code

public class StaffController : Controller 
{
[HttpGet]
[ActionName("Staff")]
public ActionResult StaffGet(int id)
{
//
}

[HttpPost]
[ActionName("Staff")]
public ActionResult StaffModify(int id, StaffMember person)
{
//
}

[HttpDelete]
[ActionName("Staff")]
public ActionResult StaffDelete(int id)
{
//
}
}

假如我们添加一个路由实体到全局里面如下:

View Code

public static void RegisterRoutes(RouteCollection routes) 
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null, "Staff/{id}",
new { controller = "Staff", action = "Staff" },
new { id = @"\d+" /* Require ID to be numeric */ });

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}

现在每个StaffMember能表单的URL唯一编址,并且一个客户端能够给使用GET,POST,和DELETE HTTP方法对这些地址执行操作。这被成为RESTful API

重写HTTP方法(Overriding HTTP Methods)

REST风格的API是伟大的,只要我们所有的客户端能够完全使用HTTP方法,服务端用.C#,JAVA,Ruby都可以。

在主流的浏览器使用Javascript AJAX调用也不会有什么困难。不幸的是,一些主流的Web技术仅仅支持POST和GET方法。这包括HTML表单和Flash应用程序。另外一些防火墙也只允许GET和POST两种请求通过。为了弥补这个缺点,在我们包含了必须的HTTP方法地方有一个约定,即使请求不能使用某一个方法,它也会被包含在请求的键值对里面,key是X-HTTP-Method-Override,值是我们期望的方法。这个key能够作为HTTP请求头(HTTP Header),HTML表单隐藏域,Querystring的一部分被指定。

例如,一个POST请求包含了一个请求头,这个请求头指定了MVC程序即使收到一个DELETE请求也应该执行。在这背后的想法是大多数web技术允许设置任意的头或表单输入,即使它们不支持同一种HTTP方法。

在MVC HTML表单重写HTTP方法(Overriding HTTP Methods in an MVC HTML Form)

MVC框架在视图和控制器提供了对HTTP方法重写的支持,视图支持意味着我们能够使用来自HTML表单的REST风格API;控制器支持意味着MVC框架会处理包含X-HTTP-Method-Override属性的请求。下面是一个在视图重写HTTP方法的示例:

View Code

... 
@using (Html.BeginForm())
{
@Html.HttpMethodOverride(HttpVerbs.Delete)
"submit" value="Delete" />
}
...

Html.HttpMethodOverride辅助方法让我们可以指定想要用来处理请求的HTTP方法,上面的示例指定的是DELETE方法,这个方法会添加一个Input到表单,如:

MVC框架在处理表单时会寻找这个Input元素,并且经过Request.GetHttpMethodOverride方法获取后这个重写的HTTP方法是可用的。MVC框架仅仅对POST请求这次HTTP方法重写。

使用专门的控制器改善性能(Improving Performance with Specialized Controllers)

MVC框架提供了两种专门的控制器来改善MVC Web应用程序。像所有的优化一样,在功能和性能之间进行折衷处理。

使用无会话的控制器(Using Sessionless Controllers)

默认情况下,控制器支持Session状态,这样能够跨请求保存数据。创建和维护Session状态就是必须涉及的处理过程,数据要被保存和检索,Session本身也要管理,让它们在合适的时间能够过期。Session的数据会消耗服务器的内存或空间并且需要跨多个Web服务器同步数据使得我们在服务器集群上运行程序更加艰难。

为了简化Session状态,ASP.NET对给定的Session一次仅仅处理一个请求,如果客户端制造多个重叠的请求,它们讲话排队还次序被服务器处理。这样做的好处是我们不用担心多个请求修改相同的数据,不好的地方是不能获得一个较好的请求吞吐量。

不是所有的控制器都使用Session状态功能,在不是用Session状态的情况下,我们可以通过避免对Session状态的维护来改善程序的性能。这时就可以使用无会话的控制器,除了两个地方跟常规的控制器不同以外,其他都一样。两个不一样的地方是:a.MVC框架不会加载和持久化Session状态当他们被用来处理请求的时候  b.重叠的请求同时被处理

在自定义的IControllerFactory里面管理Session状态(Managing Session State in a Custom IControllerFactory)

IControllerFactory接口里面包含了一个GetControllerSessionBehavior的方法,它从SessionStateBehavior返回一个枚举值,枚举里面包含了控制Session状态的四个值:Default,Required,ReadOnly,Disabled。实现了IControllerFactory接口的控制器工厂可以通过GetControllerSessionBehavior方法里面的返回一个SessionStateBehavior枚举值来设置Session状态。示例如下:

View Code

public SessionStateBehavior GetControllerSessionBehavior( 
RequestContext requestContext, string controllerName)
{
switch (controllerName)
{
case "Home":
return SessionStateBehavior.ReadOnly;
case "Other":
return SessionStateBehavior.Required;
default:
return SessionStateBehavior.Default;
}
}

使用DefaultControllerFactory管理Session状态(Managing Session State Using DefaultControllerFactory)
当我们使用内置的控制器工厂时,可以通过使用SessionState特性来控制Session状态。如下所示:

View Code

using System.Web.Mvc;
using System.Web.SessionState;

namespace SpecialControllers.Controllers
{
[SessionState(SessionStateBehavior.Disabled)]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
}

这个时候如果我们视图设置一个Session的值像这样:Session[“Message”] = “Hello”;在视图里面添加Message: @Session[“Message”] 则会抛出异常。如果设置为ReadOnly那么就只能读取其他controller设置的Session的值。当然如果我们仅仅是为了向View传递数据,那完全可以使用ViewBag或者是ViewData。

使用异步控制器(Using Asynchronous Controllers)

ASP.NET平台的核心维护着一个处理客户端请求的.NET线程池——工作者线程池(worker thread pool),里面的线程称为工作者线程(worker threads).当接收一个请求时,就会从线程池里面取一个线程并分配处理这个请求的任务。当请求被处理完毕,线程又被放回到线程池里面,以致于我们可以接着使用处理其他的请求。
使用线程池有两个重要的益处:

a.通过线程的重复使用,我们避免一些创建新的线程的额外的开销。
b.拥有固定数量可用的线程能避免服务器同时处理超越其最大请求范围的请求

当请求能够在短时间内被处理时,线程池会发挥最大的效用。这也是符合大多数MVC程序的(大多数都能在很短的时间被处理),然而,如果有控制器依赖其他的服务器并且需要花费很长的时间才能完成,这样可以能会出现这种情况:所有的线程都因为等待其他系统完成它们的工作而被占用。其实这些等待的时间本来可以用来做更多的事情。解决这个问题的方法就是使用异步控制器,这回改善应用程序的整体性能,但是对异步操作的执行是没有什么益处的。

注:异步控制器仅仅对I/O,网络绑定,CPU不密集的的action有用。我们正在视图使用异步控制器解决的问题是池模型和正在被处理的请求类型不匹配。线程池企图确保每一个请求获得恰当的服务器资源片,但却被我们弄成了一套线程都无事可做。如果我们使用另外的后台线程来处理CPU使用密集的action,那么又淡化了服务器资源能跨越多个同时请求的效果。

创建一个异步控制器(Creating an Asynchronous Controller)

下面是一个耗时请求的示例,如下:

View Code

using System.Web.Mvc;
using SpecialControllers.Models;

namespace SpecialControllers.Controllers
{
public class RemoteDataController : Controller
{
public ActionResult Data()
{
RemoteService service = new RemoteService();
string data = service.GetRemoteData();
return View((object)data);
}

}
}

//Model里面的代码
using System.Threading;

namespace SpecialControllers.Models
{
public class RemoteService
{
public string GetRemoteData()
{
Thread.Sleep(10000);
return "Hello from the other side of the world";
}
}
}

//视图代码
@model string
@{
ViewBag.Title = "Data";
}
Data:@Model

看了这个例子反映的问题,接着我们通过创建异步控制器来解决它。

创建异步控制器有两种方式:

a.实现 System.Web.Mvc.Async.IAsyncController interface接口,这里不打算使用这种方式,因为这需要非常多关于.NET并行编程的阐释,这里我们只专注于MVC本身。

b.从System.Web.Mvc.AsyncController(已经实现上面的接口)派生。其实这里类似于前面定制控制器的两种方式,一种是实现接口,另一种是从已经实现了接口的类派生我们自己的控制器类。

一个异步控制器的示例如下:

View Code

using SpecialControllers.Models;
using System.Threading.Tasks;

namespace SpecialControllers.Controllers
{
public class RemoteDataController : AsyncController
{
//public ActionResult Data()
//{
// RemoteService service = new RemoteService();
// string data = service.GetRemoteData();
// return View((object)data);
//}
public void DataAsync()
{
AsyncManager.OutstandingOperations.Increment();
Task.Factory.StartNew(() =>
{
//创建一个模型对象
RemoteService service = new RemoteService();
//调用I/O绑定,耗时的方法
string data = service.GetRemoteData();
AsyncManager.Parameters["data"] = data;
AsyncManager.OutstandingOperations.Decrement();
});
}

public ActionResult DataCompleted(string data)
{
return View((object)data);
}
}
}

创建异步和已完成方法(Creating the Async and Completed Methods)

异步action方法是成对出现的,第一个是Async(像上面的DataAsync方法)
Async里,我们可以创建并开始异步操作。当一个针对异步控制器的请求到来时,action调用者就知道action方法具有两部分并调用Async方法。在我们的例子中,当导航到/RemoteData/Data URL时,会调用DataAsync.异步方法一般返回类型是void,调用的Async方法会使用线程来执行。

方法的另一部分是:Completed.例子里面是DataCompleted.这个方法是在所有的异步操作完成后调用并且准备返回一个结果到客户端。

启动异步任务(Starting Asynchronous Tasks)

在异步方法里面创建我们所有的异步任务并告诉MVC框架我们启动了多少操作,下面对DataAsync方法进行分析:
首先需要告诉MVC框架启动了一个异步操作,这是通过AsyncManager类调用 OutstandingOperations.Increment方法完成。

其次我们要实际地创建并启动异步操作。.NET里面有多种方式来实现异步活动,这里选择的是.NET4引入的Task Parallel Library (TPL)。

下面我看下在一个异步action里面有多个异步任务的例子:

View Code

public void DataAsync() 
{
AsyncManager.OutstandingOperations.Increment(3);
Task.Factory.StartNew(() => {
// ...perform async operation here
AsyncManager.OutstandingOperations.Decrement();
});

Task.Factory.StartNew(() => {
// ...perform async operation here
AsyncManager.OutstandingOperations.Decrement();
});

Task.Factory.StartNew(() => {
// ...perform async operation here AsyncManager.OutstandingOperations.Decrement();
});
}

上面有3个异步任务,在最开始指定3个任务AsyncManager.OutstandingOperations.Increment(3); 然后在每一个任务的结尾处调用Decrement();

完善异步任务(Finishing Asynchronous Tasks)

MVC框架无从得知我们在Async方法里从事的内容,所以我们需要给它一些需要的信息,这也是为什么需要在异步方法的结尾调用AsyncManager。Increment()能够告诉MVC框架我们启动了多少任务,在每一个任务完成后,数量减一(Decrement).当未解决任务的数量为0时,MVC框架知道我们想要异步方法做的所有工作已经完成,并且准备返回结果给客户端。这时会指派一个来自线程池的线程通过调用Completed方法完成对请求的处理。

从Async向Completed方法传递参数(Passing Parameters from the Async to the Completed Method)

MVC框架会实现通常的逻辑,为来自请求的异步方法创建参数,但是我们要负责为Completed方法创建参数。方式是使用Parameters键值对集合,示例里面的 AsyncManager.Parameters[“data“] = data;给data参数赋值了,在public ActionResult DataCompleted(string data){}使用,我们要确保AsyncManager.Parameters里面的参数类型跟Completed方法里参数类型一致。如果不一致不会抛异常,而是参数的值会使用默认值(例如null)。

超时管理(Managing Timeouts)

默认情况下,MVC框架给定45秒来完成所有的异步操作,如果45秒内没有完成所有操作,请求会被放弃,并抛出超时异常。超时异常处理的方式跟其他异常一样,但如果想在特殊的情形下处理超时,可以重写OnException方法。如下所示:

View Code

protected override void OnException(ExceptionContext filterContext) 
{
if (filterContext.Exception is System.TimeoutException)
{
filterContext.Result = RedirectToAction("TryAgainLater");
filterContext.ExceptionHandled = true;
}
}

上面例子在超时异常处理是重定向页面。我们也可以通过AsyncTimeout特性来修改超时的时间区间。如
[AsyncTimeout(10000)] //10秒
public void DataAsync() {…}
也可以使用NoAsyncTimeout来完全忽略超时,这样做是不好的。因为可能某个操作失败了,也有可能用户已经放弃等待了。

放弃异步操作(Aborting Asynchronous Operations)

可以在我们的任务里面通过调用AsyncManager.Finish方法来放弃异步操作。这也就是告诉MVC框架我们准备立刻调用Completed方法并且会忽略任何未解决的操作,任何已经使用AsyncManager.Parameters集合设置的参数值会传递给Completed方法,在Finish方法被调用的时候没有被设置值的参数会使用其类型的默认值(object类型是null,数值类型是0等等)
注:调用AsyncManager.Finish方法并没有终止任何异步操作,而且它们仍然在运行。它们会正常完成,但是创建的参数值会被忽略。

使用.NET异步编程模型(Using the .NET Asynchronous Programming Pattern)

.NET框架支持不同的异步编程方式,一种是异步编程模型APM(Asynchronous Programming Model),有很多有用的.NET类遵循APM,可以通过方法名Begin和End很容易发现。我们通过调用Begion方法来启动异步操作,传递一个在操作完成时的回调,传递给回调的参数是一个对IAsyncResult接口的实现,这个会传递给End方法获取操作结果。这种方式不是很直接,但是可以使用Lambda表达式来简化代码。下面的示例是关于APM方式的:

View Code

using System.Web.Mvc;
using SpecialControllers.Models;
using System.Threading.Tasks;
using System.Net;
using System.IO;

namespace SpecialControllers.Controllers
{
public class RemoteDataController : AsyncController
{
//public ActionResult Data()
//{
// RemoteService service = new RemoteService();
// string data = service.GetRemoteData();
// return View((object)data);
//}
//public void DataAsync()
//{
// AsyncManager.OutstandingOperations.Increment();
// Task.Factory.StartNew(() =>
// {
////创建一个模型对象
// RemoteService service = new RemoteService();
////调用I/O绑定,耗时的方法
// string data = service.GetRemoteData();
// AsyncManager.Parameters["data"] = data;
// AsyncManager.OutstandingOperations.Decrement();
// });
//}

//public ActionResult DataCompleted(string data)
//{
// return View((object)data);
//}

public void PageAsync()
{
AsyncManager.OutstandingOperations.Increment();
WebRequest req = WebRequest.Create("http://www.cnblogs.com");
req.BeginGetResponse((IAsyncResult ias) =>
{
WebResponse resp = req.EndGetResponse(ias);
string content = new StreamReader(resp.GetResponseStream()).ReadToEnd();
AsyncManager.Parameters["html"] = content;
AsyncManager.OutstandingOperations.Decrement();
}, null);
}

public ContentResult PageCompleted(string html)
{
return Content(html, "text/html");
}
}
}

//添加Page视图
@{

ViewBag.Title = "Page";
}

Page


//接着运行程序在URL输入/RemoteData/Page

决定何时使用异步控制器(Deciding When to Use Asynchronous Controllers)

异步控制器相对于通常的控制器要复杂的多,它需要许多额外的代码并且很难阅读和维护。异步控制器要保守使用而且仅仅在下面的情况下使用:

a.action是I/O绑定并且不是CPU绑定
b.经常因为线程池耗尽而出问题时
c.我们自己接受创建异步控制器时产生的额外的复杂度
d.我们已经尝试过相对简单的解决方案,例如缓存等

上面并不是让我们惧怕并远离异步控制器,但是它们的确也用在解决小范围的问题,而这些问题是大多数应用程序不会遭受的问题,并且引入它们的复杂性很难合理化。

好了,本章的笔记已经完成。
祝大家愉快!

发表评论

电子邮件地址不会被公开。 必填项已用*标注