[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

继上一篇文章之后,本文将介绍 Controller 和 Action 的一些较高级特性,包括 Controller Factory、Action Invoker 和异步 Controller 等内容。

本文目录

开篇:示例准备

文章开始之前,我们先来了解一下一个请求的发出到Action方法处理后返回结果的流程,请试着理解下图:

本文的重点是 controller factory 和 action invoker。顾名思义,controller factory 的作用是创建为请求提供服务的Controller实例;action invoker 的作用是寻找并调用Action方法。MVC框架为这两者都提供了默认的实现,我们也可以对其进行自定义。

首先我们为本文要演示的示例做一些准备,把暂时想到的要用的 View、Controller 和 Action 都创建好。新建一个空的MVC应用程序,在Models文件夹中添加一个名为 Result 的Model,代码如下:

namespace MvcApplication2.Models {
    public class Result {
        public string ControllerName { get; set; }
        public string ActionName { get; set; }
    }
}

在 /Views/Shared 文件夹下添加一个名为 Result.cshtml 的视图(不使用Layout),添加代码如下:

...
<body>
    <div>Controller: @Model.ControllerName</div> 
    <div>Action: @Model.ActionName</div> 
</body>

本文的所有Action方法将都使用这同一个View,目的是显示被执行的Controller名称和Action名称。

然后我们创建一个名为Product的Controller,代码如下:

public class ProductController : Controller {
        
    public ViewResult Index() {
        return View("Result", new Result {
            ControllerName = "Product",
            ActionName = "Index"
        });
    }

    public ViewResult List() {
        return View("Result", new Result {
            ControllerName = "Product",
            ActionName = "List"
        });
    }
}

继续添加一个名为Customer的Controller,代码如下:

public class CustomerController : Controller {
        
    public ViewResult Index() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "Index"
        });
    }

    public ViewResult List() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "List"
        });
    }
}

准备工作做好了,开始进入正题吧。

自定义 Controller Factory

Controller Factory,顾名思义,它就是创建 controller 实例的地方。想更好的理解Controller Factory是如何工作的,最好的方法就是自己去实现一个自定义的。当然,在实际的项目中我们很少会去自己实现,一般使用内置的就足够。自定义一个Controller Factory需要实现 IControllerFactory 接口,这个接口的定义如下:

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

我们创建一个名为 Infrastructure 文件夹,在这个文件夹中创建一个名为 CustomControllerFactory 的类文件,在这个类中我们将简单的实现 IControllerFactory 接口的每个方法,代码如下:

using System;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using MvcApplication2.Controllers;

namespace MvcApplication2.Infrastructure {
    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();
            }
        }
    }
}

先来分析一下这个类。

这里最重要的方法是 CreateController,当MVC框架需要一个 Controller 来处理请求时调用该方法。它有两个参数,一个是 RequestContext 对象,通过它我们可以得到请求相关的信息;第二个参数是一个string类型的controller名称,它的值来自于URL。这里我们只创建了两个Controller,所以我们在 CreateController 方法中进行了硬编码(写死了controller的名称),CreateController 方法的目的是创建Controller实例。

在自定义的Cotroller Factory中,我们可以任意改变系统默认的行为,比如switch语句中的default节点:

requestContext.RouteData.Values["controller"] = "Product";

它将路由的controller值改为Product,使得执行的cotroller并不是用户所请求的controller。

在本系列的 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 文章中也讲了一个用 Ninject 创建Controller Factory的例子,使用的是 ninjectKernel.Get(controllerType) 方法来创建Controller实例。这里我们使用 MVC 框架提供的 DependencyResolver 类来创建:

return targetType == null ? null : (IController)DependencyResolver.Current.GetService(targetType);

静态的 DependencyResolver.Current 属性返回一个 IDependencyResolver 接口的实现,这个实现中定义了 GetService 方法,它根据 System.Type 对象(targetType)参数自动为我们创建 targetType 实例,和使用Ninject类似。

最后来看看实现 IControllerFactory 接口的另外两个方法。

GetControllerSessionBehavior 方法,告诉MVC框架是否保留Session数据,这点放在文章后面讲。

ReleaseController 方法,当controller对象不再需要时被调用,这里我们判断controller对象是否实现了IDisposable接口,实现了则调用 Dispose 方法来释放资源。

CustomControllerFactory 类分析完了。和 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 讲的示例一样,要使用自定义的Controller Factory还需要在 Global.asax.cs 文件的 Application_Start 方法中对自定义的 CustomControllerFactory 类进注册,如下:

protected void Application_Start() {
    ...
    ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
}

运行程序,应用程序根据路由设置的默认值显示如下:

你可以定位到任意 /xxx/xxx 格式的URL来验证我们自定的 Controller Factory 的工作。

使用内置的 Controller Factory

 为了帮助理解Controller Factory是如何工作,我们通过实现IControllerFactory接口自定义了一个Controller Factory。在实际的项目中,我们一般不会这么做,大多数情况我们使用内置的Controller Factory,叫 DefaultControllerFactory。当它从路由系统接收到一个请求后,从路由实例中解析出 controller 的名称,然后根据名称找到 controller 类,这个类必须满足下面几个标准:

  • 必须是public。
  • 必须是具体的类(非抽象类)。
  • 没有泛型参数。
  • 类的名称必须以Controller结尾。
  • 类必须(间接或直接)实现IController接口。

DefaultControllerFactory类维护了一个满足以上标准的类的列表,这样当每次接收到一个请求时不需要再去搜索一遍。当它找到了合适的 controller 类,则使用Controller Activator(一会介绍)来创建Controller 类的实例。它内部是通过 DependencyResolver 类进行依赖解析创建 controller 实例的,和使用Ninject是类似的原理。

你可以通过继承 DefaultControllerFactory 类重写其中默认的方法来自定义创建 controller 的过程,下面是三个可以被重写的方法:

  • GetControllerType,返回Type类型,为请求匹配对应的 controller 类,用上面定义的标准来筛选 controller 类也是在这里执行的。
  • GetControllerInstance,返回是IController类型,作用是根据指定 controller 的类型创建类型的实例。
  • CreateController 方法,返回是 IController 类型,它是 IControllerFactory 接口的 CreateController 方法的实现。默认情况下,它调用 GetControllerType 方法来决定哪个类需要被实例化,然后把controller 类型传递给GetControllerInstance。

重写 GetControllerInstance 方法,可以实现对创建 controller 实例的过程进行控制,最常见的是进行依赖注入。

在本系列的 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 文章中的示例就是一个对 GetControllerInstance 方法进行重写的完整示例,在这就不重复演示了。

现在我们知道 DefaultControllerFactory 通过 GetControllerType 方法拿到 controller 的类型后,它把类型传递给 GetControllerInstance 方法以获取 controller 的实例。那么,GetControllerInstance 又是如何来获取实例的呢?这就需要讲到另外一个 controller 中的角色了,它就是下面讲的:Controller Activator。

Controller 的激活

当 DefaultControllerFactory 类接收到一个 controller 实例的请求时,在 DefaultControllerFactory 类内部通过 GetControllerType 方法来获得 controller 的类型,然后把这个类型传递给 GetControllerInstance 方法以获得 controller 的实例。

所以在 GetControllerInstance  方法中就需要有某个东西来创建 controller 实例,这个创建的过程就是 controller 被激活的过程。

默认情况下 MVC 使用 DefaultControllerActivator 类来做 controller 的激活工作,它实现了 IControllerActivator 接口,该接口定义如下:

public interface IControllerActivator { 
    IController Create(RequestContext requestContext, Type controllerType); 
}

该接口仅含有一个 Create 方法,RequestContext 对象参数用来获取请求相关的信息,Type 类型参数指定了要被实例化的类型。DefaultControllerActivator 类中整个 controller 的激活过程就在它的 Create 方法里面。下面我们通过实现这个接口来自定义一个简单的 Controller Activator:

public class CustomControllerActivator : IControllerActivator {
    public IController Create(RequestContext requestContext, Type controllerType) {
        if (controllerType == typeof(ProductController)) {
            controllerType = typeof(CustomerController);
        }
        return (IController)DependencyResolver.Current.GetService(controllerType);
    }
}

这个 CustomControllerActivator 非常简单,如果请求的是 ProductController 则我们给它创建 CustomerController 的实例。为了使用这个自定的 Activator,需要在 Global.asax 文件中的 Application_Start 方法中注册 Controller Factory 时给 Factory 的构造函数传递我们的这个 Activator 的实例,如下:

protected void Application_Start() { 
    ...
    ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new CustomControllerActivator())); 
} 

运行程序,把URL定位到 /Product ,本来路由将指定到 Product controller, 然后 DefaultControllerFactory 类将请求 Activator 创建一个 ProductController 实例。但我们注册了自义的 Controller Activator,在这个自定义的 Activator 创建 Controller 实例的的时候,我们做了一个“手脚”,改变了这种默认行为。当请求创建 ProductController 实例时,我们给它创建了CustomerController 的实例。结果如下:

其实更多的时候,我们自定义 controller 的激活机制是为了引入IoC,和 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 讲的通过继承 DefaultControllerFactory 引入 IoC 是一个道理。

自定义 Action Invoker

 当 Controller Factory 创建好了一个类的实例后,MVC框架则需要一种方式来调用这个实例的 action 方法。如果创建的 controller 是继承 Controller 抽象类的,那么则是由 Action Invoker 来完成调用 action 方法的任务,MVC 默认使用的是 ControllerActionInvoker 类。如果是直接继承 IController 接口的 controller,那么就需要手动来调用 action 方法,见上一篇 [ASP.NET MVC 小牛之路]09 - Controller 和 Action (1) 。下面我们通过自定义一个 Action Invoker 来了解一下 Action Invoker 的运行机制。

创建一个自定义的 Action Invoker 需要实现 IActionInvoker 接口,该接口的定义如下:

public interface IActionInvoker { 
    bool InvokeAction(ControllerContext controllerContext, string actionName); 
} 

这个接口只有一个 InvokeAction 方法。ControllerContext 对象参数包含了调用该方法的controller的信息,string类型的参数是要调用的Action方法的名称,这个名称来源于路由系统。返回值为bool类型,当actoin方法被找到并被调用时返回true,否则返回false。

下面是实现了IActionInvoker接口的 CustomActionInvoker 类:

using System.Web.Mvc;

namespace MvcApplication2.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;
            }
        }
    }
}

这个 CustomActionInvoker 不需要关心实际被调用的Action方法。如果请求的是Index Action,这个 Invoker 通过 Response 直接输出一个消息,如果不是请Index Action,则会引发一个404-未找到错误。

决定Controller使用哪个Action Invoker是由 Controller 中的 Controller.ActionInvoker 属性来决定的,由它来告诉MVC当前的 controller 将使用哪个 Action Invoker 来调用 Action 方法。如下我们创建一个ActionInvokerController,并在它的构造函数中指定了 Action Invoker 为我们自定义的 Action Invoker:

namespace MvcApplication2.Controllers {
    public class ActionInvokerController : Controller {
        public ActionInvokerController() {
            this.ActionInvoker = new CustomActionInvoker();
        }
    }
}

这个 controller 中没有 Action 方法,它依靠 CustomActionInvoker 来处理请求。运行程序,将URL定位到 /ActionInvoker/Index 可见如下结果:

如果将URL定位到 ActionInvoker 下的其他Action,则会返回一个404的错误页面。

我们不推荐去实现自己的Action Invoker。首先内置的Action Invoker提供了一些非常有用的特性;其次是缺乏可扩展性和对View的支持等。这里只是为了演示和理解MVC框架处理请求过程的细节。

使用内置的 Action Invoker

通过自定义 Action Invoker,我们知道了MVC调用 Action 方法的机制。我们创建一个继承自 Controller 抽象类的 controller,如果不指定Controller.ActionInvoker,那么MVC会使用内置默认的Action Invoker,它是 ControllerActionInvoker 类。它的工作是把请求匹配到对应的 Action 方法并调用之,简单说就是寻找和调用 Action 方法。

为了让内置的 Action Invoker 能匹配到 Action 方法,Action方法必须满足下面的标准:

  • 必须是公共的(public)。
  • 不能是静态的(static)。
  • 不能是System.Web.Mvc.Controller中存在的方法,或其他基类中的方法。如方法不能是 ToString 和 GetHashCode 等。
  • 不能是一个特殊的名称。所谓特殊的名称是方法名称不能和构造函数、属性或者事件等的名称相同。

注意,Action方法也不能带有泛型,如MyMethod<T>(),虽然 Action Invoker 能匹配到,但会抛出异常。

内置的 Action Invoker 给我们提供了很多实用的特性,给开发带来很大便利,下面两节内容可以说明这一点。

给 Action 方法定义别名

默认情况下,内置的Action Invoker (ControllerActionInvoker)寻找的是和请求的 action 名称相同的 action 方法。比如路由系统提供的 action 值是 Index,那么 ControllerActionInvoker 将寻找一个名为 Index 的方法,如果找到了,它就用这个方法来处理请求。ControllerActionInvoker 允许我们对此行为进行调整,即可以通过使用 ActionName 特性对 action 使用别名,如下对 CustomerController 的 List action 方法使用 ActionName 特性:

public class CustomerController : Controller {
    ...
    [ActionName("Enumerate")]
    public ViewResult List() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "List"
        });
    }
}

当请求 Enumerate Action 时,将会使用 List 方法来处理请求。下面是请求 /Customer/Enumerate 的结果:

这时候对 /Customer/List 的请求则会无效,报“找不到资源”的错误,如下:

使用 Action 方法别名有两个好处:一是可以使用非法的C#方法名来作为请求的 action 名,如 [ActionName("User-Registration")]。二是,如果你有两个功能不同的方法,有相同的参数相同的名称,但针对不同的HTTP请求(一个使用 [HttpGet],另一个使用 [HttpPost]),你可以给这两个方法不同的方法名,然后使用 [ActionName] 来指定相同的 action 请求名称。

Action 方法选择器

我们经常会在 controller 中对多个 action 方法使用同一个方法名。在这种情况下,我们就需要告诉 MVC 怎样在相同的方法名中选择正确的 action 方法来处理请求。这个机制称为 Action 方法选择,它在基于识别方法名称的基础上,允许通过请求的类型来选择 action 方法。MVC 框架可使用C#特性来做到这一点,所以这种作用的特性可以称为 Action 方法选择器。

内置 Action 方法选择器

MVC提供了几种内置的特性来支持 Action 方法选择,它包括HttpGet、HttpPost、HttpPut 和 NonAction 等。这些选择器从名字上很容易理解什么意思,这里就不解释了。下面举个 NonAction 的例子。在 CustomerController 中添加一个 MyAction 方法,然后应用 [NonAction] 特性,如下:

public class CustomerController : Controller {
    ...
    [NonAction]
    public ActionResult MyAction() {
        return View();
    }
}

使用 [NonAction] 后,方法将不会被识别为 action 方法,如下是请求 /Customer/MyAction 的结果:

当然我们也可以通过把方法声明为 private 来告诉MVC它不是一个 action 方法。

自定义 Action 方法选择器

除了使用内置的Action方法选择器外,我们也可以自定义。所有的 action 选择器都继承自 ActionMethodSelectorAttribute 类,这个类的定义如下:

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

它是一个抽象类,只有一个抽象方法:IsValidForRequest。通过重写这个方法,可以判断某个请求是否允许调用 Action 方法。

我们来考虑这样一种情况:同一个URL请求,在本地和远程请求的是不同的 action (如对于本地则绕过权限验证可能需要这么做)。那么自定义一个本地的 Action 选择器会是一个不错的选择。下面我们来实现这样一个功能的 Action 选择器:

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

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

修改 CustomerController,添加一个LocalIndex 方法,并对它应用 “Index”别名,代码如下:

public class CustomerController : Controller {
        
    public ViewResult Index() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "Index"
        });
    }

    [ActionName("Index")]
    public ViewResult LocalIndex() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "LocalIndex"
        });
    }
    ...          
}

这时如果请求 /Customer/Index,这两个 action 方法都会被匹配到而引发歧义问题,程序将会报错:

这时候我们再对 LocalIndex 应用我们自定义的 Local 选择器:

...
[Local]
[ActionName("Index")]
public ViewResult LocalIndex() {
    return View("Result", new Result {
        ControllerName = "Customer",
        ActionName = "Index"
    });
}
...

程序在本地运行的时候则会匹配到 LocalIndex action方法,结果如下:

通过这个例子我们也发现,定义了选择器特性的Action方法被匹配的优先级要高于没有定义选择器特性的Action方法。

异步 Controller

对于 ASP.NET 的工作平台 IIS,它维护了一个.NET线程池用来处理客户端请求。这个线程池称为工作线程池(worker thread pool),其中的线程称为工作线程(worker threads)。当接收到一个客户端请求,一个工作线程从工作线程池中被唤醒并处理接收到的请求。当请求被处理完了后,工作线程又被这个线程池回收。这种线程程池的机制对ASP.NET应用程序有如下两个好处:

  • 通过线程的重复利用,避免了每次接收到一个新的请求就创建一个新的线程。
  • 线程池维护的线程数是固定的,这样线程不会被无限制地创建,减少了服务器崩溃的风险。

一个请求是对应一个工作线程,如果MVC中的action对请求处理的时间很短暂,那么工作线程很快就会被线程池收回以备重用。但如果执行action的工作线程需要调用其他服务(如调用远程的服务,数据的导入导出),这个服务可能需要花很长时间来完成任务,那么这个工作线程将会一直等待下去,直到调用的服务返回才继续工作。这个工作线程在等待的过程中什么也没做,资源浪费了。设想一下,如果这样的action一多,所有的工作线程都处于等待状态,大家都没事做,而新的请求来了又没人理,这样就陷入了尴尬境地。

解决这个问题需要使用异步(asynchronous) Controller,异步Controller允许工作线程在等待(await)的时候去处理别的请求,这样做减少了资源浪费,有效提高了服务器的性能。

使用异步 Controller 需要用到.NET 4.5的新特性:异步方法。异步方法有两个新的关键字:await 和 async。这个新知识点朋友们自己去网上找找资料看吧,这里就不讲了,我们把重点放在MVC中的异步 Controller 上。

在Models文件夹中添加一个 RemoteService 类,代码如下:

using System.Threading;
using System.Threading.Tasks;

namespace MvcApplication2.Models {

    public class RemoteService {

        public async Task<string> GetRemoteDataAsync() {
            return await Task<string>.Factory.StartNew(() => {
                Thread.Sleep(2000);
                return "Hello from the other side of the world";
            });
        }
    }
}

然后创建一个名为 RemoteData 的 Controller,让它继承自 AsyncController 类,代码如下:

using System.Web.Mvc;
using MvcApplication2.Models;
using System.Threading.Tasks;

namespace MvcApplication2.Controllers {
    public class RemoteDataController : AsyncController {
        public async Task<ActionResult> Data() {
            
            string data = await new RemoteService().GetRemoteDataAsync();
            Response.Write(data);
            
            return View("Result", new Result {
                ControllerName = "RemoteData",
                ActionName = "Data"
            });
        }
    }
}

运行程序,URL 定位到 /RemoteData/Data,2秒后将显示如下结果:

当请求 /RemoteData/Data 时,Data 方法开始执行。当执行到下面代码调用远程服务时:

string data = await new RemoteService().GetRemoteDataAsync();

工作线程开始处于等待状态,在等待过程中它可能被派去处理新的客户端请求。当远程服务返回结果了,工作线程再回来处理后面的代码。这种异步机制避免了工作线程处于闲等状态,尽可能的利用已被激活的线程资源,对提高MVC应用程序性能是很有帮助的。、

评论精选

提问 by 卤鸽

IActionInvoker是在Controller.Excute方法中被调用,主要是查询具体的Action 处理方法,其实整个请求的过程都是从MvcHandler进行开始的。这是我的理解。不知正确否?

回答 by Liam Wang

是这么个意思,但不太严谨。确切一点说,Excute 方法是(自定义或默认的)ActionInvoker 的入口函数。 ActionInvoker 必须实现 IActionInvoker 接口来查找和调用 Action 方法。本文没有介绍 MvcHandler 的知识。MvcHandler 是处理Controller的开始,但在MvcHandler 之前还有一个MvcRouteHandler,当请求经过路由解析后,MvcRouteHandler 实例会生成一个 MvcHandler 实例,并把请求交给它。MvcHandler 会从Controller 工厂中获取一个 Controller 实例,然后由 Controller 来具体地处理请求。

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值