MVC中引导动作方法执行过程的请求流程管道中,有两个重要的部件:控制器工厂(Controller Factory) 和 动作调用器(Action Invoker)。控制器工厂负责创建对请求进行服务的控制器实例,动作调用器负责查找并调用控制器类中的动作方法。MVC框架中含有这两个组件的默认实现,可以配置并控制他们的行为,也可以完全替换这些组件。
准备示例项目
新建一个空的MVC项目名叫ControllerExtensibility的项目。在Model中添加一个Result.cs文件,代码如下图所示:
namespace ControllerExtensibility.Models
{public class Result{public string ControllerName { get; set; }public string ActionName { get; set; }}
}
在/Views/Shared文件夹下,添加一个名称为Result.cshtml的视图 ,代码如下:
@model ControllerExtensibility.Models.Result
@{Layout = null;
}<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><title>Result</title>
</head>
<body><div>Controller:@Model.ControllerName</div><div>Action:@Model.ActionName</div>
</body>
</html>
新增两个控制器, Product控制器和Customer控制器,代码如下图所示:
public class CustomerController : Controller{// GET: Customerpublic ActionResult Index(){return View("Result",new Result { ControllerName = "Customer",ActionName = "Index" });}public ViewResult List(){return View("Result", new Result { ControllerName = "Customer",ActionName = "Index" });}}
public class ProductController : Controller{// GET: Productpublic ActionResult Index(){return View("Result",new Result { ControllerName = "Product",ActionName = "Index" } );}public ViewResult List(){return View("Result", new Result { ControllerName = "Product", ActionName = "List" });}}
这些控制器不执行任何有用的动作,只是通过Result.cshtml视图报告他们已经被调用了。
创建自定义控制器工厂
控制器工厂是由IControllerFactory接口定义的,如下图所示:
public interface IControllerFactory{IController CreateController(RequestContext requestContext, string controllerName);SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName);void ReleaseController(IController controller);}
这个接口中最重要的方法是CreateController,当MVC框架需要控制器对请求进行服务时,便会调用这个方法。该方法的一个参数是一个RequestContext对象,它让工厂能够检测请求的细节,另一个参数是一个字符串,它包含了从路由的URL那里得到所得到的controller值。
GetControllerSessionBehavior 方法由MVC框架用来确定是否应该为控制器维护会话数据。
当不在需要CreateController,方法创建的控制器对象时,会调用ReleaseController方法释放资源。
下面简单创建了一个控制器工厂,代码如下:
public class CustomControllerFactory : IControllerFactory{public IController CreateController(RequestContext requestContext, string controllerName){Type targetType = null;switch (controllerName){case "Product":targetType = typeof(ProductController);break;case "Customer":targetType = typeof(CustomerController);break;default:requestContext.RouteData.Values["controller"] = "Product";targetType = typeof(ProductController);break;}return targetType == null ? null : (IController)DependencyResolver.Current.GetService(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();}}}
以上自定义控制器工厂只会指向 名叫Product 和 Customer 控制器,并且如果控制器不是这两个,就默认指向Product控制器。
静态的DependencyResolver.Current属性返回IDependencyResolver接口的实现。该接口定义了GetService方法,为方法传递了一个System.Type对象。这里可以理解为实例化一个目标类型对象。
注册使用自定义控制器工厂
通过ControllerBuilder类,可以告诉MVC框架使用这个自定义的控制器工厂。在Global.asax.cs文件中的Application_Start方法中加入如下代码即可:
protected void Application_Start()
{AreaRegistration.RegisterAllAreas();RouteConfig.RegisterRoutes(RouteTable.Routes);ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());//注册
}
一旦注册了控制器工厂,将由它负责处理请求应用程序接收到的所有请求,启动程序,就可以看到如下结果:
使用内建的控制器工厂
对于大多数程序应用程序,内建的控制器工厂类DefaultControllerFactory完全足够满足需求。当它从路由系统接收到一个请求时,该工厂考察路由数据,找到 Controller 属性的值,并企图在这个Web 应用程序中找到满足如下条件的类:
1、这个类必须是一个public类。
2、这个类必须是具体类(不是抽象类)。
3、这个类必须没有泛型参数。
4、这个类必须以Contoller结尾。
5、这个类必须实现IController接口。
DefaultControllerFactory类里有这些类的一个列表,一个请求到达时,它并不需要每次都执行一次搜索。如果找到了,便用控制器激活器(Controller Activator)创建一个实例。
命名空间优先排序
如果有同名控制器位于不同命名空间的,需要对命名空间优先排序,在Global.asax.cs文件中的Application_Start方法中加入如下代码即可:
ControllerBuilder.Current.DefaultNamespaces.Add("MyControllerNamespace");
ControllerBuilder.Current.DefaultNamespaces.Add("MyProject.*");
所有添加命名空间的顺序,并不暗示搜索顺序或者相对优先级—— 所有Add方法定义的命名空间一视同仁。而优先级是相对于那些没有Add的方法。如果控制器在Add方法中定义的命名空间找不到合适的控制器,那就会搜搜整个应用程序。
上述代码中第二句的“ * ”表示的是查询MyProject命名空间及所包含的子命名空间。
定制DefaultControllerFactory的控制器实例化
也可以通过创建一个控制器激活器(Controller Activator)对一个指定一个控制器类型进行实例化。代码如下图所示:
public class CustomerControllerActivator : IControllerActivator{public IController Create(RequestContext requestContext, Type controllerType){if (controllerType == typeof(ProductController)){controllerType = typeof(CustomerController);}return (IController)DependencyResolver.Current.GetService(controllerType);}}
此IControllerActivator的实现很简单——如果请求的是ProductController类,将以CustomerController类的实例作为其响应。
为了激活这个自定义控制器,也需要在在Global.asax.cs文件中的Application_Start方法中加入如下代码:
ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new CustomerControllerActivator()));
启动程序并导航到/Product,效果如下图所示:
重写DefaultControllerFactory方法
可以重写DefaultControllerFactory类中的方法,来控制控制器的创建。
方法 | 结果 | 描述 |
CreateController | IController | IControllerFactory接口的CreatController方法的实现。默认情况下,这个方法调用GetControllerType来确定应该实例化哪个类型,然后通过将结果传递给GetControllerInstance方法,来获得一个控制器对象。 |
GetControllerType | Type | 将请求映射到控制器类型。 |
GetControllerInstance | IController | 创建指定类型的一个实例。 |
创建自定义动作调用器
一旦控制器工厂创建了一个(控制器)类的实例,框架就需要一种办法来调用这个实例上的一个动作。如果控制器是通过Controller类派生的,那么将由动作调用器(Action Invoker)调用动作。
动作调用器实现IActionInvoke接口,如下图所示:
public interface IActionInvoker{bool InvokeAction(ControllerContext controllerContext, string actionName);}
该接口只有一个单一的成员:InvokeAction(调用动作) 。其返回值是一个布尔类型的值,返回true,表示找到并调用了这个动作方法;false表示控制器没有匹配的动作。
新增一个CustomActionInvoker.cs文件继承此接口,代码如下图所示:
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;}}}
这个动作方法并不关心控制器类中的方法。它只处理自己的动作。如果这是对Index动作的请求,那么该调用器直接将一条消息写到Response。如果是对其他动作的请求,则返回false,这将会导致一个“404——未找到”的错误消息给用户。
与一个控制器相关联的动作调用器是通过Controller.ActionInvoker属性获得的,同一个应用程序中的不同控制器可以试用版不同的动作调用器。新增一个 ActionInvoker的新控制器,代码如下:
public class ActionInvokerController : Controller{public ActionInvokerController() {this.ActionInvoker = new CustomActionInvoker();}}
这个控制器中没有动作方法,它依靠动作调用器去处理请求。通过启动程序,并导航到/ActionInvoker/Index,可以看到其工作情况,而导航同一个控制器中的其他方法则看到404错误。 如下图所示:
使用内建的动作调用器
内建的动作调用器ControllerActionInvoker类,有一些将请求与动作方法进行匹配的非常完善的技术。默认的动作调用器是依靠方法进行操作的。为了具备一个动作的资格,一个方法必须满足如下几个条件:
1、该方法是必须是public的。
2、该方法必须不是staticd的。
3、该方法必须不是在 System.Web.Mvc.Controller或它的任何基类中。
4、该方法没有专用名。
前两个条件很简单。第三个条件排除了Controller类或其基类出现的方法,这意味着不包括ToString及GetHashCode这样的方法,因为这些都是IController接口实现的方法。最后一个条件意味着排除了构造器、属性以及事件访问器。
注:具有泛型参数的方法(如 MyMethod<T>() 满足所有条件,但是如果视图调用这样的方法吹里一个请求,MVC框架会报错)。
默认情况下,ControllerActionInvoker 查找一个具有与请求的动作同名的方法。 而且,MVC框架提供了一些可以调整的方法。
使用自定义动作名
通常,动作方法的名称确定了它所表示的动作。Index动作方法对Index动作进行服务。但是可以用ActionName注解属性来重写这一行为。如下图所示:
public class CustomerController : Controller{// GET: Customerpublic ActionResult Index(){return View("Result",new Result { ControllerName = "Customer",ActionName = "Index" });}[ActionName("Enumerate")]public ViewResult List(){return View("Result", new Result { ControllerName = "Customer",ActionName = "Index" });}}
导航到/Customer/Enumerata,效果如下图所示:
这一注解属性重写了动作的名称,这意味着导航到List方法不再工作,如下图所示:
以这种方式重写方法名的原因主要有两个:
1、可以接收一个作为C# 方法名不合法的动作名,例如【ActionName(“User-Registration”】,其中“-”符号在C#中是不合法的。
2、如果希望有两个不同的C#方法接受同一组参数,并且运用同样的动作名(具有同样参数的方法不能实现重载,只能采用不同的方法名),但是要对不同的HTTP请求类型进行响应,例如一个是【HttpGet】,而另一个是【HttpPost】那么可以对这些方法用不同的C#名来满足编译器的要求,然后用【ActionName】将他们映射到同一个动作名。
使用动作方法选择
很多情况是一个控制器中含有几个同名的动作,这可能是因为有多个方法,每个方法的参数个数不同。或者是使用[ActionName]注解属性,使多个方法表示同一个动作。
动作调用器在选择一个动作时,会利用动作方法选择器来消除不确定性。比如【HttPost】注解属性就是一个动作方法的选择器。首先会评估带动作方法选择器的动作,以考察其是否适合处理该请求。
【HttpGet】用于Get请求,【HttpPost】用于Post请求,另一个内建的注解属性是NonAction(非动作),它向动作调用器解释不应该作为作为动作方法来使用。如下图所示:
[NonAction]public ActionResult MyAction(){return View();}
上述代码中的MyAction方法,将不会被看成是一个动作。以NonAction方法为目标的URL请求会生成“404——未找到”错误。另一个通常的方法是把这些方法标记为Private。
创建自定义动作方法选择器
动作方法选择器派生于ActionMethodSelectorAttribute类,如下图所示:
//// 摘要:// 表示一个特性,该特性用于影响操作方法的选择。[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]public abstract class ActionMethodSelectorAttribute : Attribute{//// 摘要:// 初始化 System.Web.Mvc.ActionMethodSelectorAttribute 类的新实例。protected ActionMethodSelectorAttribute();//// 摘要:// 确定操作方法选择对指定的控制器上下文是否有效。//// 参数:// controllerContext:// 控制器上下文。//// methodInfo:// 有关操作方法的信息。//// 返回结果:// 如果操作方法选择对指定的控制器上下文有效,则为 true;否则为 false。public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo);}
ActionMethodSelectorAttribute是一个抽象类,它定义了一个抽象方法:IsValidForRequest。该方法的一个参数是controllercontext对象,用来对请求进行检测,另一个参数是MethodInfo对象,用来获取运用了选择器方法的信息。如果该方法能处理请求,便返回true,否则便返回false。
如下图中创建了一个简单的自定义选择器,代码如下图所示:
public class LocalAttribute : ActionMethodSelectorAttribute{public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo){return controllerContext.HttpContext.Request.IsLocal;}}
将该动作方法选择器运运用,创建了Home控制器,代码如下图所示:
public class HomeController : Controller{// GET: Homepublic ActionResult Index(){return View("Result",new Result { ControllerName = "Home",ActionName = "Index"});}[ActionName("Index")]public ActionResult LocalIndex(){return View("Result", new Result { ControllerName = "Home", ActionName = "LocalIndex" });}}
上述代码创建了两个Index动作方法,因此,当/Home/Index请求达到时,动作调用器无法猜出应该使用哪一个,就会报错:
对Home控制器运用注解属性:
public class HomeController : Controller{// GET: Homepublic ActionResult Index(){return View("Result",new Result { ControllerName = "Home",ActionName = "Index"});}[Local][ActionName("Index")]public ActionResult LocalIndex(){return View("Result", new Result { ControllerName = "Home", ActionName = "LocalIndex" });}}
如果重启程序,并从本地机器上运行浏览器导航到根URL,将会看到MVC框架已经考虑了方法的选择注解属性。解决了控制器类中方法之间的歧义问题:
处理未知动作
如果动作方法调用器找不到一个要调用的动作方法,便从它的InvokerAction方法返回false,当这种情况发生时,Controller类会调用它的HandleUnknowAction方法,默认情况下,这个方法会将一个“404——未找到”响应给客户端。这是控制器大多数应用程序所能做的最有用的事情。如果想做一些特殊的事情,可以在控制器类中选择重写这个方法。代码如下图所示:
public class HomeController : Controller{// GET: Homepublic ActionResult Index(){return View("Result",new Result { ControllerName = "Home",ActionName = "Index"});}[Local][ActionName("Index")]public ActionResult LocalIndex(){return View("Result", new Result { ControllerName = "Home", ActionName = "LocalIndex" });}protected override void HandleUnknownAction(string actionName){Response.Write(string.Format("You requested the {0} action ",actionName));}}
导航到一个不存在动作,如下图所示:
使用无会话控制器
默认情况下,控制器是支持会话状态的,这可以用来跨请求地存取数据值,使MVC程序员的工作更轻松。创建和维护会话状态是一个棘手的过程。必须对数据进行存储和接收,且必须对会话进行管理,以使他们能适当地终止。会话数据会消耗服务器内存或一些其他存储单元空间。而且多个Web服务器之间的数据同步的需求,使得在服务器场(server farm)上运行应用程序更加困难。
为了简化会话状态,ASP.NET 对一个给定的会话在某一个时刻只处理一个查询,如果客户形成了多个重叠的请求,它们将被排成队列,并由服务器依次处理。其好处是不需要担心多个请求对同一数据进行修改的情况,缺点是得不到所希望的请求的吞吐量。
并非所有控制器都需要这种会话状态特性。在这种情况下,能够改善应用程序的性能,而又避免了棘手的会话状态维护工作。这可以通过无会话控制器来实现。它们与规则控制器一样,但是有两个方面不同:在把它们用于处理一个请求时,MVC框架不加载或不存储会话状态,重叠请求可以同时处理。
IControllerFactory接口中,含有一个叫做“SessionStateBehavior”的方法,该方法返回SessionStateBehavior枚举中的一个值。如下图所示:
值 | SessionStateBehavior枚举的值 |
Default | 使用默认的ASP.NET行为,它会根据HttpContext来决定会话状态的配置 |
Required | 启用完全会话状态 |
ReadOnly | 启用只读会话状态 |
Disable | 完全禁用会话状态 |
通过返回GetControllerSessionBehavior方法的SessionStateBehavior的值,实现IControllerFactory接口的控制器工厂会直接设置控制器会话状态的行为。 传递给这个方法的参数是RequestContext 和一个含有控制器名称的字符串,可以返回如上图中任意一个值,也可以根据不同的控制器返回不同的值,如下图所示:
public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{switch (controllerName){case "Home":return SessionStateBehavior.ReadOnly;case "Prouduct":return SessionStateBehavior.Required;default:return SessionStateBehavior.Default;}
}
用DefaultControllerFactory管理会话状态
当使用内建的控制器工厂(MVC 应用程序默认使用的就是这个默认的控制器工厂DefaultControllerFactroy)时,可以将SessionState注解属性运用于每个控制器类,以便对控制器的会话进行控制,如下图所示:
[SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)]
public class FastController : Controller
{// GET: Fastpublic ActionResult Index(){return View("Result", new Result { ControllerName = "Fast",ActionName = "Index"});}
}
上述控制器运用了SessionState注解属性,它影响着该控制器的所有动作。 Disable完全禁用了会话状态,如果在控制中设置了一个会话值:
Session[" Message "] = " Hello "
如果想从其他地方试图读取这个值,@Session[" Message "] ,MVC框架会报错。HttpContext.Session属性会返回Null。
如果制定了Readonly,那么可以读取从其他控制器设置的值,但是企图修改,也会报错。
使用异步控制器
核心ASP.NET 平台维护着一个用来处理客户端请求的.NET 线程池。这个线程池叫做“工作线程池(Work Thread Pool)”,而这些线程叫做“工作线程(Work Thread)”。当接受到一个请求时,将占用线程池中的一个工作线程,以进行这个请求的处理工作。当请求处理完成后,该工作线程被返回给线程池,以便用于新请求的处。对ASP.NET应用程序使用线程池有两个好处:
1、通过重用工作线程,避免了每次处理一个请求时,都要创建一个新的线程的开销(创建线程是需要时间的,若采用现有的线程就不一样了)。
2、通过具有固定数目的可用工作线程,避免了超出服务器处理能力的并发请求情况。
在请求可以被短时间处理完毕的情况下,工作线程池工作的最好。这也是大多数MVC应用程序的情况。但是,如果有一些依赖于其他的服务器且占用较长时间才能完成的动作,那么你可能会遇到所有工作线程都被绑定于等待其他系统完成其工作的情况。
此刻服务器有能力做更多的工作——毕竟,这只是在等待,只占用了很少的资源——但是因为所有线程都被绑定,传入的请求都被排成队列。这将陷入应用程序处理停顿,而服务器大片的闲置的奇怪状态。
这一问题的解决方案是使用异步控制器,这是提高应用程序的整体性能,但是不利于执行异步操作(即可提高性能,但实现(异步操作)难)。
注意:编写并发代码容易,编写能够正常工作的并发代码是及其困难的。最好使用默认的线程池。特别是对于新手。即便是老手,也应该知道,编写和测试一个新的线程池所付出的努力,与得到的回报是相比,是微不足道的。
异步控制器只能对占用I / O 或占用网络带宽,而且非CPU密集型的动作是有用的(CPU密集型动作是指,需要CPU高负荷运转,占用较多内存,执行大量处理才能完成的动作)。 异步控制器解决的问题应当是,线程池与所处理的请求类型之间搭配不当的状态。线程池意在确保每个请求到得到一片服务器资源,但是很可能停滞于一组无所事事的工作线程上。如果对CPU密集型动作使用额外的后台线程,那么会因为涉及太多的并发请求而削弱服务器资源。
创建一个RemoteData常规同步控制器,如下图所示:
public class RemoteDataController : Controller{// GET: RemoteDatapublic ActionResult Index(){return View();}public ActionResult Data(){RemoteService service = new RemoteService();string data = service.GetRemoteData();return View((object)data);}}
RemoteService 实例代码如下图所示:
public class RemoteService{public string GetRemoteData(){Thread.Sleep(2000);return "Hello from the other side of world";}}
添加对动作Data的新视图,如下图所示 :
@model string
@{Layout = null;
}<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><title>Data</title>
</head>
<body><div> Data:@Model</div>
</body>
</html>
运行效果如下图所示:
创建异步控制器
使用关键字await 和 async,创建一个新的Task对象,并await它的响应。修改Data动作器代码如下图所示:
public async Task<ActionResult> Data()
{RemoteService service = new RemoteService();string data = await Task<string>.Factory.StartNew( () => { return new RemoteService().GetRemoteData(); });return View((object)data);
}
在控制器中使用异步方法
也可以在其他地方通过异步控制器来使用异步方法,在RemoteService.cs中添加如下方法:
public class RemoteService
{public string GetRemoteData(){Thread.Sleep(2000);return "Hello from the other side of world";}public async Task<string> GetRemoteDataAsync(){return await Task<string>.Factory.StartNew(() => { Thread.Sleep(2000); return "Hello from the other side of the world"; });}
}
在控制器中调用异步方法:
public class RemoteDataController : Controller
{// GET: RemoteDatapublic ActionResult Index(){return View();}public async Task<ActionResult> Data(){RemoteService service = new RemoteService();string data = await Task<string>.Factory.StartNew( () => { return new RemoteService().GetRemoteData(); });return View((object)data);}public async Task<ActionResult> ConsumeAsyncMethod(){string data = await new RemoteService().GetRemoteDataAsync();return View("Data", (object)data);}
}