ASP.NET MVC

ASP.NET MVC 路由(Routing)

System.Web.Routing命名空间下的有关路由的类型

此命名空间下定义了与路由机制有关的类型,首先从宏观上了解一下这些类型。

RouteTable //路由表
RouteCollection //存储在路由表中的路由集合 通过RouteTable.Routes获取
Route //存储在路由集合中的某一个具体的路由 通过RouteTable.Routes[index]获取
RouteData //存储某一个具体的路由中的路由数据 通过RouteTable.Routes[index].GetRouteData( )获取
RouteValueDictionary //存储在路由数据中的字典对象,通过RouteTable.Routes[index].GetRouteData( ).Values获取 该对象可以以键值对的方式获取segment变量的值 

RouteTable(路由表)

表示全局路由的类

Routes
//获取路由集合,返回一个RouteCollection

RouteCollection(路由集合)

表示路由列表的类,此类通过RouteTable.Routes获得。

Ignore ( )
//如果RouteExistingFiles是默认的false,则任何文件都可以通过请求该文件的物理地址从而得到这个文件
//如果RouteExistingFiles被手动设为true,那么任何文件都不再可以通过请求该文件的物理地址得到文件
//所以在RouteExistingFiles为true的情况下,如果我们想增加一些例外,比如你不希望css或js文件的请求需要通过路由地址访问,
//那么此时才有使用Ignore()方法的必要。
//注意,Ignore()方法必须放在路由注册的前面,否则无效
//示例:
routes.RouteExistingFiles = true//请求的任何文件必须使用路由地址访问
routes.Ignore ( "{resource}.css/{*pathInfo}" ); //指示css文件不必通过路由地址访问
 
IgnoreRoute ( )
//与Ignore()方法等同
 
MapPageRoute ( )
//添加一个路由,将Url地址映射为与路由模板匹配的路由地址,比如将一个aspx页面地址映射为与路由模板匹配的路由地址,这样你可以通过路由地址对default.aspx进行访问。
//示例:
RouteTable.Routes.MapPageRoute ( "" , "employees/{name}/{id}/{*other}" , "~/Default.aspx" , true , defaults );
//有了以上的针对具体某一个页面(default.aspx)的路由映射,现在可以直接在浏览器输入http://localhost/employees/sam/1001访问到default.aspx
 
MapRoute ( )
//RouteCollectionExtentions为RouteCollection定义的扩展方法,此方法需要引用System.Web.Mvc程序集。
//用于在ASP.NET MVC框架下添加一个路由,区别于MapPageRoute ( )方法
//此方法并非将某一页面的物理地址映射为一个与路由模板匹配的路由地址,它是定义了一个与任何页面进行匹配的路由模板,这个模板必须具有Contrller和Action变量。
//以下使用MapRoute ( )方法添加了一个路由
//注意,MapRoute()方法的Defaults、Constraint参数不再是RouteValueDictionary,而是object,这是为了编程的便利性,最终它们还是会被路由机制自动转化为RouteValueDictionary
//示例:注册一个路由
routes.MapRoute (
    name: "Default" ,
    url: "{controller}/{action}/{id}" ,
    defaults: new { controller = "Home" , action = "Index" , id = UrlParameter.Optional }
);

//当请求的路由地址被服务端接收后,服务端将在全局路由范围内查找匹配的路由模板
//假设在另一个项目或不同命名空间下存在同名的Controller,那么路由机制如何确定应将请求路由到哪个Controller中呢?
//通过指定namespaces可以使路由查找机制优先考虑使用namespaces所指定的Controller
routes.MapRoute (
    name: "Default" ,
    url: "{controller}/{action}/{id}" ,
    namespaces: new string [ ] { "My.Controllers" } ,  //优先使用My.Controllers命名空间下的Controller
    defaults: new { controller = "Home" , action = "Index" , id = UrlParameter.Optional }
);

GetVirtualPath ( RequestContext requestContext,RouteValueDictionary values )
//遍历全局路由列表,查找与values存储的路由变量完全匹配的路由
//如果匹配成功则返回一个VirtualPathData对象,否则返回null。通过VirtualPathData.VirtualPath可以获取到匹配成功后的Url
//示例:在某个页面中:
string employeeID = RouteData.Values [ "id" ] as string;
System.Web.Routing.RequestContext context = new System.Web.Routing.RequestContext ( );
context.HttpContext = new HttpContextWrapper ( HttpContext.Current );
context.RouteData = RouteData;
string s = RouteData.Route.GetVirtualPath ( context , RouteData.Values ).VirtualPath; //return like:/employees/sam/1001
System.Web.HttpContext.Current.Request.FilePath
 
GetVirtualPath ( RequestContext requestContext,RouteValueDictionary values , string RouteName )
//根据参数指定的路由名称找到该路由,将values存储的路由变量与该路由的路由模板进行匹配
//如果匹配成功则返回一个VirtualPathData对象,否则返回null。通过VirtualPathData.VirtualPath可以获取到匹配成功后的路由地址
 
GetRouteData(RequestContext httpContext);
//此方法将从路由表中查找与当前请求的Url匹配的路由
//如果请求的Url成功匹配了路由模板则返回与当前请求相关的、存储了路由信息的RouteData实例
//如果匹配是失败的,则返回null
//示例:手动创建一个HttpContextBase,通过代码来模拟一次客户端的请求以便获得RouteData
HttpRequest request = new HttpRequest ( "/Home/Index" , "http://localhost:53326/Home/Index" , null );
HttpResponse response = new HttpResponse ( new System.IO.StringWriter ( ) );
HttpContext context = new HttpContext ( request , response );
HttpContextBase contextWraper = new HttpContextWrapper ( context );          
routeData =RouteTable.Routes.GetRouteData ( contextWraper );
 
RouteExistingFiles
//如果将此属性设为true,则所有Url请求都将强制路由匹配。默认false,也即在默认情况下,Url地址的模式可以是某个文件地址的模式也可以是路由模板的模式
//比如假设现在请求的是aspx页面,在浏览器直接输入此页面的地址:http://localhost/default.aspx,因为默认情况下RouteExistingFiles是false,所以对这个页面的直接请求并不会进入路由匹配。
View Code

Route(路由)

表示具体某一个路由的类  

Defaults;
//一个RouteValueDictionary泛型字典集合,用于为路由的路由模板设置变量的默认值
//示例:
Route route = new Route ( "employees/{name}/{id}/{*other}" , new PageRouteHandler ( "~/default.aspx" );
route.Defaults= new RouteValueDictionary
{
    { "name","sam" },
    { "id","1001" }
};
 
Constraints;
//一个RouteValueDictionary泛型字典集合,用于为路由设置约束,约束用于验证某些自定义的逻辑,没通过验证则路由会失败
route.Constraints = new RouteValueDictionary
{
    { "controller" , @"d{4}" }, //为路由模板的controller变量设置了正则式约束
    {  "httpMethod"new HttpMethodConstraint ( "GET" ) } //设置了请求约束,请求必须是Get请求
}; 

DataTokens
//一个RouteValueDictionary的泛型字典集合,存储了路由的命名空间
//通过RouteData.DataTokens["Namespaces"]或Route.DataTokens["Namespaces"]可以获取路由所在的命名空间
//如果注册非区域路由时没有在MapRoute中指定namespaces,那么RouteData.DataTokens["Namespaces"]或Route.DataTokens["Namespaces"]都==null
//如果在注册区域路由时没有在MapRoute中指定namespaces,那么路由机制会默认将区域类的命名空间的字符表示拼接一个".*"的字符作为后缀存储在DataTokens["Namespaces"]中
//如:RouteData.DataTokens["Namespaces"]= MVC.Areas.FirstArea.*
 
GetRouteData ( )
//将请求的Url和路由模板进行匹配,如果匹配失败,返回null,否则返回一个RouteData实例
 
GetVirtualPath ( RequestContext requestContext,RouteValueDictionary values )
//将values存储的路由变量与当前路由的路由模板进行匹配,如果匹配成功则返回一个VirtualPathData对象,否则返回null。通过VirtualPathData.VirtualPath可以获取到匹配成功后的路由地址
 
GetRequiredString ( string key )
//在RouteData的Values中查找路由变量所对应的值,key是路由变量的名称。注意,如果key不存在则会抛出异常。如果要获取查询字符串则依然是使用Request["查询字符变量"]
 
Url
//获取路由对象的模板,即类似这样的路由模板:employees/{name}/{id}
View Code

RouteData(路由数据)

表示存储路由有关信息的类,当请求的Url地址被成功解析后会生成一个RouData对象,此对象可以通过在Controller类或Page类中的RouData属性访问得到。 

Route
//获取路由对象,返回一个RouteBase

Values
//一个RouteValueDictionary的泛型字典集合,存储了路由变量的默认值。Key是路由变量的名称,Value是变量的值。
//这是在RouteCollection的MapRoute方法中通过defaults参数配置的对象
//存储的是模板变量的默认值
//请求的Url如果与路由模板匹配成功则此字典存储的变量默认值会被替换为Url中的与Key对应的变量值
//如果Key不存在则会抛出异常
//如果要获取查询字符串则依然是使用Request["查询字符变量"]

DataTokens
//一个RouteValueDictionary的泛型字典集合,存储了路由的命名空间
//通过RouteData.DataTokens["Namespaces"]或Route.DataTokens["Namespaces"]可以获取路由所在的命名空间
//如果注册非区域路由时没有在MapRoute中指定namespaces,那么RouteData.DataTokens["Namespaces"]或Route.DataTokens["Namespaces"]都==null
//如果在注册区域路由时没有在MapRoute中指定namespaces,那么路由机制会默认将区域类的命名空间的字符表示拼接一个“.*”的字符作为后缀存储在DataTokens["Namespaces"]中
//如:RouteData.DataTokens["Namespaces"]= Mvc.Areas.FirstArea.* 

RouteHandler
//一个实现了IRouteHandler接口的Http处理程序,每一个成功匹配了路由模板的Url请求都由这个Http处理程序进行处理,这个处理程序默认是MvcRouteHandler
//该对象提供接口方法GetHttpHandler(),该方法可以返回一个用于处理当前路由请求的处理程序,此属性用于设置或获取可处理当前路由请求的IRouteHandler对象
//当然,开发人员可以自定义HttpHandler处理程序,以便可以处理特殊的Url请求,详细参看:ASP.NET MVC - ASP.NET请求处理机制浅析

GetRequiredString(string valueName)
//获取segment变量的值
string controllerName = requestContext.RouteData.GetRequiredString( "controller" );
View Code

RouteValueDictionary(路由变量字典)

路由模板中的变量都存储在字典中,在ASP.NET MVC中,一个路由模板必须包含名称为Controller和Action的变量,以下两种方式都可以向字典添加键值对。 
object obj = new { controller = "default" ,action="index"};
RouteValueDictionary routeValue = new RouteValueDictionary ( obj );
var controllerName= routeValue [ "controller" ];
var actionName = routeValue [ "action" ];
//或
RouteValueDictionary routeValue2= new RouteValueDictionary
{
    { "controller","default" },
    { "action","index" }
}
View Code

VirtualPathData(Url地址)

表示Url虚拟地址 

Route
//获取路由对象

VirtualPath
//获取已经成功匹配路由模板的Url,如:employee/3001
View Code 

RequestContext(特殊的Http上下文)

此类封装了当前的路由数据和Http请求的上下文

HttpContext
//获取一个HttpContextBase类型的表示当前Http请求的上下文

RouteData
//当前的路由信息
View Code

 

ASP.NET中的路由

ASP.NET本身就具有路由的机制,只不过没有得到使用,当发起一个请求时是直接请求的该文件的物理地址。其实在System.Web.Routing命名空间中就有一些关于路由的类型,我们也可以使用这些类型将Url请求映射为路由地址,一个传统的Url请求可能是这样的:

http://www.cnblogs.com/employees/GetList.aspx

而ASP.NET MVC的Url地址是这样的:

http://www.cnblogs.com/employees/GetList

传统的Url请求中的employees是文件夹,GetList则是具体的物理文件,而ASP.NET MVC的Url请求中的employees不是目录而是一个控制器类,GetList则是一个Action。这种Url会被路由机制截获并把地址用来和路由列表里的路由进行匹配,匹配成功则会初始化控制器并调用相应的Action方法。使用这种Url地址的好处显而易见,它隐藏了真实文件的地址,而且便于搜索引擎优化。

 

ASP.NET MVC中的路由

ASP.NET MVC也使用System.Web.Routing命名空间中路由类,但它与ASP.NET WEB Forms有区别,在System.Web.Mvc程序集中有一个RouteCollectionExtentions类,此类为RouteCollection定义了一系列的扩展方法专门用于ASP.NET MVC框架下的路由机制。 

注册路由

打开App_Start目录下的RouteConfig.cs,可以在RegisterRoutes方法中注册路由。

首先打开代表HttpApplication的Global.asax文件,可以看到如下的代码:

protected void Application_Start( )
{
    AreaRegistration.RegisterAllAreas( );
    //当Web应用程序首次开启运行时会调用位于App_Start目录下的RouteConfig.RegisterRoutes方法以便将添加的路由注册到路由集合中(RouteCollection)
    //向该方法传递了一个空的路由集合
    //在该方法中开发人员可以使用这个集合的MapRoute方法(相当于集合的Add方法)向集合添加路由            
    RouteConfig.RegisterRoutes( RouteTable.Routes );
}

而MapRoute方法的源代码如下:

//在MapRoute方法的源代码中可以看到如下信息:
//MapRoute方法是RouteCollectionExtentions类为RouteCollection定义的扩展方法
public static Route MapRoute( this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces )
{
    //创建路由对象
    //同时创建一个MvcRouteHandler
    //而在Route的构造函数中源码中,将MvcRouteHandler对象绑定在了Route的RoutData上
    //MvcRouteHandler对象是用来创建ControllerBuilder对象(此对象会根据当前路由数据中的控制器名称创建控制器)和MvcHandler对象(此对象会处理当前的Http路由请求)
    Route route = new Route( url, new MvcRouteHandler( ) )
    {
        Defaults = new RouteValueDictionary( defaults ),
        Constraints = new RouteValueDictionary( constraints ),
        DataTokens = new RouteValueDictionary( )
    };
    return route;
}

路由匹配 

路由匹配的意思是:当用户在浏览器地址栏输入一个Url后,这个Url请求会发送到服务端,由服务端调用RouteTable.Routes.GetRouteData(HttpContextBase context),该方法从Http上下文中获取请求的Url,然后从RouteTable.Routes中查找与当前请求的Url匹配的路由对象。GetRouteData所要匹配的就是当前请求的Url是否与路由集合中的某一个具体的路由的Url属性值是一样的。该方法如果成功匹配路由则会返回一个RouteData对象。

定义路由模板

Route具有一个Url属性,该属性用于为路由设置一个路由模板

Url: "employee / { controller } / { action } / { id }"
//这是一个路由模板(也称Url模板、Url模式)
//每个被 / 分隔的段称为segment
//每个segment可以有两种类型的参数
//要么是segment常量,要么是segment变量
//segment常量就是一个文本值
//segment变量则使用:{ 变量名 },以此作为占位符 
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
        name: "Default"//设置路由名字 
        url: "{controller}/{action}/{id}"//设置路由模板
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } //设置路由segment变量默认值
    );
}

路由匹配规则

请求的Url的段数与路由模板的段数必须相等且常量值相等。

所以下图的Url可以匹配这个路由模板:Url: "employee / { controller } / { action } / { id }"

employee匹配了路由模板的常量值 

details匹配了路由模板的Controller变量,即Controller变量的值是details 

employeeDetails匹配了路由模板的Action变量,即Action变量的值是employeeDetails 

1001匹配了路由模板的Id变量,即id变量的值是1001

注:常量不会当做控制器或Action,控制器或Action必须以变量的形式定义。

控制器的激活浅析

当一个客户端的请求被服务端截获,路由机制会遍历全局路由列表查找与请求的Url相匹配的路由,匹配成功后会自动在所有后缀名为Controller的控制器中查找与Http路由请求中的Controller匹配的控制器类,找到后会new出控制器实例以便调用Action方法。为什么要查找?因为Url中的Controller仅仅是一个控制器的名称,一个字符串没有办法new出Controller实例。

控制器的约定

控制器可以创建在项目下的任何一个自定义的目录中或使用默认的Controller目录,但控制器名称必须以Controller结尾,否则无法注册该控制器,MVC框架也就无法new出控制器实例。

为路由指定控制器的命名空间

如果在默认的控制器目录(Controller目录)中创建了一个AController的控制器,接着又在自定义的Test目录创建一个AController控制器,那么客户端请求的Url可能是这样:/A/List,现在有两个叫做A的Controller,路由机制对于应激活哪一个Controller是无法判断的,此时应考虑在添加路由时显示指定控制器类所在的命名空间。

routes.MapRoute(
    name: "Default",
    namespaces: new string[] { "Test.Controllers" }, //优先考虑Test目录下的控制器
    url: "{controller}/{action}/{id}/{*test}"
);

 如果在指定的命名空间找不到控制器则会继续查找其它控制器(称为后续查找),如果你想禁止路由机制匹配当前命名空间以外的控制器,则可以作如下设置:

routes.MapRoute(
    name: "Default",
    namespaces: new string[] { "Test.Controllers" }, //优先考虑Test目录下的控制器
    url: "{controller}/{action}/{id}/{*test}"
).DataTokens["UseNamespaceFallback"= false//默认是true,设为false后,路由机制就只会在当前命名空间中匹配控制器

路由模板的变量默认值

Route具有一个Defaults的属性,设置Defaults是预防当请求的Url没有提供变量值时将使用默认值填充路由模板,如果请求的Url提供了变量值则不会应用Defaults。

Url: "employee / { controller } / { action } / { id }"
defaults: new { controller = "Home" , action = "Index" }
//如果请求的Url没有提供controller变量的值和action变量的值则使用默认值进行填充

因为请求的Url没有提供变量值,所以使用了默认值对模板进行填充,模板最终生成的地址是:employees/Home/Index

默认值UrlParameter.Optional

如果不确定要为变量设置怎样的默认值,那就将变量的默认值设为UrlParameter.Optional即可。

url: "{controller}/{action}/{id}" ,
defaults: new { controller = "Home" , action = "Index" , id = UrlParameter.Optional }

无需匹配的变量值

使用*号定义的变量不会参与匹配,如:

url: "Files / { controller } / { action } / { name } / { *otherSegments }"
//请求的Url如果没有最后一个段,也能匹配,如果请求的Url提供了otherSegments的值。那么这样的变量也会存储在RouteValueDictionary字典中,可通过Key获取

路由约束

Route具有一个constraints属性,该属性用于为路由设置约束。有两种方式可以添加路由约束,一是使用正则式,二是自定义一个约束类 

正则式约束

//约束了controller变量的值必须是4个数字 同时该请求必须是GET的
constraints: new { controller = @"d{4}", httpMethod = new HttpMethodConstraint ("GET") },

自定义约束

自定义约束类必须实现IRouteConstraint接口,实现的Match()方法返回true表示通过验证。以下创建了一个LocalhostConstraint的类,它实现的Match()方法用于检测请求是否来自本地计算机,如果不是则不匹配路由。

public class LocalhostConstraint : IRouteConstraint
{
    public bool Match ( HttpContextBase httpContext , Route route , string parameterName , RouteValueDictionary values , RouteDirection routeDirection )
    {
        return httpContext.Request.IsLocal;
    }
}

接着我们需要在注册的路由上使用该约束,只需要在constraints中初始化该约束类即可  

constraints: new { LocalhostConstraint = new LocalhostConstraint ( ) }

 

区域(Area)

如果项目结构非常庞大就需要使用区域,可以把区域看成是项目的一个一个的子系统。右击项目-添加-区域即可创建一个区域,每个区域都包含了Controller、Model和View目录。比如以下创建了一个名称为FirstArea的区域,区域默认包含在代表全部区域的总目录Areas中,此后可以右击Areas目录来创建更多的区域。

区域路由地址中的段默认会加上区域的名称,如:

每个区域都配置有一个后缀为xxxAreaRegistration的cs文件,这样的文件中都定义了一个派生自AreaRegistration类(区域基类)的子类,表示为一个区域类。在区域类的RegisterArea方法中可以在当前区域下注册路由,如下面代码所示: 

public class FirstAreaAreaRegistration : AreaRegistration 
{
    public override string AreaName 
    {
        get => "FirstArea";
    }

    public override void RegisterArea(AreaRegistrationContext context) 
    {
        context.MapRoute( //在当前区域下注册路由
            name:"FirstArea_default",
            url:"FirstArea/{controller}/{action}/{id}",
            defaults:new { action = "Index", id = UrlParameter.Optional }
        );
    }
}
View Code 

在Global文件的Application_Start事件中默认使用了AreaRegistration的静态方法将所有区域进行了统一的初始化。

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start ( )
    {
        AreaRegistration.RegisterAllAreas ( );
        //……
    }
}

与区域有关联的类

AreaRegistration(区域)

此类表示为区域基类,我们在创建一个区域时就是在创建一个AreaRegistration的派生(上面例子中的FirstAreaAreaRegistration就是一个AreaRegistration的子类)。AreaRegistration提供一个RegisterAllAreas的静态方法,此方法会扫描所有被当前项目所引用的程序集,查找所有从AreaRegistration派生的类型,同时会new出每一个区域的实例,这些实例都将被看成是AreaRegistration类型的变量。与此同时,RegisterAllAreas方法还会针对每一个区域创建它们所对应的AreaRegistrationContext实例,区域实例的RegisterArea方法需要一个AreaRegistrationContext作为参数以便注册区域路由,所以RegisterAllAreas创建出AreaRegistrationContext实例后会将其作为参数传递给对应的区域实例的RegisterArea方法以便调用AreaRegistrationContext的MapRoute方法对区域路由进行注册。

//区域FirstAreaAreaRegistration的RegisterArea方法接收一个AreaRegistrationContext
public override void RegisterArea(AreaRegistrationContext context) 
{
    context.MapRoute(
        name:"FirstArea_default",
        url:"FirstArea/{controller}/{action}/{id}",
        defaults:new { action = "Index", id = UrlParameter.Optional }
    );
}

AreaRegistrationContex(区域上下文)

此上下文对象用于注册区域路由,提供区域的信息。 

AreaName
//区域的名称

Routes
//获取所有的路由(包含区域路由),返回一个RouteCollection列表。注:如果在RegisterArea方法中通过AreaRegistrationContext直接获取路由列表的路由个数,则只返回Areas目录下所包含的区域路由个数,不包含非区域路由
//示例:
//在FirstArea区域下的Controller目录中创建了DefaultController并为DefaultController自定义了两个属性RouteList和RouteCount
namespace MVCArea.Areas.FirstArea.Controllers
{
    public class DefaultController : Controller
    {
        public static System.Web.Routing.RouteCollection  RouteList { getset; }
        public static int RouteCount { getset; }
      
        public ActionResult Index()
        {
            ViewData [ "RouteList" ] = RouteList.Count; //包含全局路由,区域路由的路由列表总个数
            ViewData [ "RouteCount" ] = RouteCount; //只有Areas目录下所包含的全部区域路由个数
            return View();
        }
    }
}
//在FirstAreaRegistration类的RegisterArea方法中通过AreaRegistrationContext访问路由列表
FirstArea.Controllers.DefaultController.RouteList = context.Routes; //返回全局路由和区域路由的路由列表,在外部调用RouteList.Count时会包含这些路由列表总个数
FirstArea.Controllers.DefaultController.RouteCount = context.Routes.Count; //在RegisterArea方法中则只返回Areas目录下所包含的全部区域路由个数

State
//一个object类型的用户自定义的对象,当在Global文件的Application_Start事件中执行AreaRegistration的重载方法RegisterAllAreas(object State)时,State将会赋值给创建出的AreaRegistrationContext的State属性

Namespaces
//这是一个只读的string[]属性,该属性自动将当前区域类的命名空间作为在它区域内的路由的命名空间并拼接了一个".*"的字符作为后缀,如:MVC.Areas.FirstArea.*
//当请求发生时,如果在全局路由列表中匹配上多个不同命名空间下的同名Controlle时将自动优先考虑Namespaces存储的命名空间下的Controller
//如果在AreaRegistrationContex的MapRoute中显示指定了路由的命名空间,则此属性会被匹配所忽略,匹配将考虑使用MapRoute中指定的命名空间
//此属性将作为Value存储在Route.DataTokens["Namespaces"]和RouteData.DataTokens["Namespaces"]中

MapRoute()
//在当前AreaRegistrationContext所关联的区域下注册一个区域性的路由
//示例:在区域类的RegisterArea方法中注册区域路由:
public override void RegisterArea ( AreaRegistrationContext context )
{
    context.MapRoute (
        name: "" ,
        url: "FirstArea/{controller}/{action}/{id}" ,
        defaults: new { action = "Index" , id = UrlParameter.Optional }
    );
}
View Code

MVC-AreaRegistrationTypeCache.xml

这是一个临时缓存的xml文档,ASP.NET MVC框架会扫描当前项目引用的程序集,利用反射将程序集中的从AreaRegistration派生的类型集合成一个列表,然后对它们进行序列化以便生成xml文档。这样,当AreaRegistration调用它的静态方法RegisterAllAreas创建区域实例的时候,就可以直接从xml文档将AreaRegistration列表反序列化为类型列表,省去了每次都要反射造成资源大量消耗的麻烦。

 

ASP.NET的Http处理管道

一些基本概念

1.进程( Process )
//在一定的内存中承载应用程序,一个进程的错误可能造成其它进程的崩溃

2.应用程序域(AppDomain)
//Net程序需要Clr进行托管以保障安全,AppDomain正是Clr创建的区块,Clr利用进程,将其划分为N块逻辑分区
//这些分区就称为AppDomain,一个进程可以被划分成N个AppDomain。
//而.Net程序集通过CLR被载入到某个AppDomain中进行处理,也即AppDomain的作用是用来承载.Net应用程序的,其优势在于以下几点:
*.内存开销
//一个AppDomain的内存开销显著低于进程,而且跨域访问对象的内存开销也要显著低于跨进程访问对象
*.内存控制
//在C#中AppDomain被表示为一个类型,它提供了卸载程序集的方法,
//而Clr正是利用AppDomain进行垃圾回收和对程序集的卸载,开发人员也可以利用AppDomain对.net程序进行内存控制
//一旦程序超载则可以手动AppDomain卸载程序集,卸载后再将程序集重新载入以便降低内存占用。(调用HttpRuntime.UnloadAppdomain()方法)
*.热插拔
//在C#中AppDomain被表示为一个类型,它提供了加载程序集的方法
//把应用程序的某个分离的功能做成一个即插即用的插件,可随时应对客户需求的变更。
*.边界隔离
//一个进程的错误可能带来其它进程的崩溃,
//比如一台服务器开10个进程挂上10个网站应用程序,一个错误就会导致10个网站全军覆没,
//而AppDomain是进程的分区,域的边界隔离机制可以将多个.Net程序隔离,使它们相互独立,
//也即把每个网站程序放在一个进程的10个域中,一个AppDomain出错永远不会影响到其它AppDomain,
//从而不会导致整个进程的崩溃。但可能多个域都需要引用某个公用的程序集,最典型的例子就是MSCorLib.dll。
//该程序集包含了基元类型等重要的.net类型,像这样的程序集不需要跨域访问,
//因为在CLR初始化时就会将这种程序集自动载入一个公共域中,这样,所有的AppDomain都共享该程序集的类型。

3.HTTP通信接口( HTTP.SYS )
//Windows的核心组件,任何基于Windows平台的应用程序都要通过它提供的API接口才能进行HTTP通信

4,应用程序池( Application Pools )
//提供给开发人员管理应用程序的工具,控制应用程序的运行,结束,内存分配,请求最大链接数等

5.工作者进程( w3wp.exe )
//Windows的进程,与应用程序池是相互为用的关系,池管理应用程序,
//而w3wp.exe承载应用程序的运行。两者相互协作,控制应用程序的生命。
//工作者进程下可以划分多个AppDomain域来承载多个web应用程序。
//既然工作者进程总是关联一个应用程序池,
//所以你也可以这样理解:程序池是一个进程,该进程从Cpu中划分了一定的内存,web程序就被限制在程序池中,一个池子可以承载多个web程序的运行。

HTTP请求的整个流程

HTTP请求最早由HTTP.SYS接收,接着传递给IIS,请求进入IIS后,IIS会测试该请求所获取的是静态还是动态资源,而早期的web总是以静态的html资源返回给客户端,后来逐渐出现了动态资源,而IIS无法处理这种请求,所以需要一些扩展,通过把请求传递给扩展程序,由扩展程序去处理请求。

静态资源

IIS直接取出html,jpg等资源后往客户端发送。

CGI资源

基于CGI协议的早期通用网关接口,可处理扩展名为cgi的动态资源的请求,如果扩展名为CGI,则IIS会开启cgi.exe进程,执行由c,vb,python,php编写的可执行文件对请求进行处理。

扩展程序

通过IIS提供的配置,可以将扩展程序注册到由IIS管理的扩展程序映射表中,非静态资源的请求进入IIS后,IIS将查找扩展程序映射表,为了便于理解,可以视此表为键值对集合,key是动态资源的扩展名,value是处理请求的扩展程序。处理HTTP请求的常见扩展程序则有以下几种:

1.asp.dll扩展程序,执行由asp编写的可执行文件对请求进行处理。

2.FASTCGI扩展程序,这是CGI进化版的扩展程序,执行由c,vb,python,php编写的可执行文件对请求进行处理。

3.aspnet_isapi.dll扩展程序,可处理各种动态资源的请求,最强。

搭建Clr运行时

IIS确认后缀名所对应的扩展程序后则会通知应用程序池,后者通知w3wp.exe加载表中注册的扩展程序以便处理Http请求,而w3wp.exe进程会先确认CLR是否已经在运行,如果没有,则将扩展程序(如:aspnet_isapi.dll)载入内存,由aspnet_isapi.dll负责搭建CLR(.Net运行时)。我们知道asp.net的程序集必须通过CLR的即时编译才能转化为cpu指令,才能够处理来自客户端请求的动态资源,所以工作者进程首先得把Clr运行时搭建出来。

创建应用程序域

Clr运行时搭建完成后,将开始创建应用程序域,( 此处需要注意,应用程序域使用的是延迟初始化机制,也即只有当第一个请求进入IIS并被有效传递给工作者进程后,域才会被创建。假如你手动关闭域以此来减轻内存开销,又或者遭遇不可控的因素致使域被关闭,那么当下一个请求被IIS传递给工作者进程后,域又会被重新初始化)。由Clr调用System.Web.Hosting命名空间下的AppDomainFactory的Create方法(.Net.20时代是调用AppManagerAppDomainFactory.Create)在当前工作者进程中创建出AppDomain对象和ISAPIRuntime对象。

创建与Http请求相关联的对象

域创建完成后,工作者进程将把HTTP请求传递给处于域中的ISAPIRuntime对象,而该对象会调用自身的ProcessRequest方法,该方法根据原始的Http报文创建一个从HttpWorkerRequest派生的IsapiWorkerRequest对象,此对象就代表了Http请求的报文信息,接着该方法还会调用HttpRuntime的ProcessRequestNoDemand方法,该方法会调用其它方法,后续一系列的方法调用的逻辑都是为了能创建出HttpContext、HttpContext.Response、HttpContext.Request、HttpApplication、HttpHandler、HttpModule。当HttpApplication管道搭建起来之后,就会自动触发管道事件,这些事件全部与Http请求有关,如果某个HttpModule订阅了这些事件,那么该处理模块就会得到执行,而当PostResolveRequestCache事件执行完成之后,处理当前请求的HttpHandler也会得到执行这些方法的部分源码如下: 

//HttpRuntime.ProcessRequestNoDemand的部分源码
//此方法的作用:将Http请求报文对象添加到Http请求队列中
internal static void ProcessRequestNoDemand( HttpWorkerRequest wr )
{
    //RequestQueue是存储IsapiWorkerRequest的队列集合
    //获取Http请求报文的队列集合
    RequestQueue queue = _theRuntime._requestQueue;
    //更新Http请求报文计数器,这将当前的Http请求报文添加到了集合中(个人臆测)
    wr.UpdateInitialCounters( );
    if (queue != null)
    {
        //再从队列中获取当前的Http请求报文
        wr = queue.GetRequestToExecute( wr );
    }
    //ProcessRequestNow方法内部会将IsapiWorkerRequest参数传递给ProcessRequestInternal方法
    ProcessRequestNow( wr );

}

//HttpRuntime.ProcessRequestInternal的部分源码
//此方法的作用:根据Http请求的报文创建HttpContext、HttpContext.Response、HttpContext.Request对象、执行每一个注册好的Http请求的管道事件
private void ProcessRequestInternalHttpWorkerRequest wr )
{
    HttpContext context;
    //根据IsapiWorkerRequest创建HttpContext对象
    //而在HttpContext的构造函数中会创建出Request和Response的实例
    try { context = new HttpContext( wr, false ); }
    catch
    {
        try
        {
            //400错误,请求无效
            wr.SendStatus( 400"Bad Request" );
            //将错误信息输出
            wr.SendKnownResponseHeader( 12"text/html; charset=utf-8" );
            byte[] data = Encoding.ASCII.GetBytes( "<html><body>Bad Request</body></html>" );
            wr.SendResponseFromMemory( data, data.Length );
            wr.FlushResponse( true );
            wr.EndOfRequest( );
            return;
        }
    }

    //GetApplicationInstance方法内部会调用GetNormalApplicationInstance方法创建HttpApplication
   //HttpApplication创建完成,再创建HttpModule、发布管道事件
    //以上全部完成之后,再根据当前请求的扩展名获取一个对应的Http处理程序(HttpHandler),扩展名所对应的HttpHandler注册在web.config中
    IHttpHandler applicationInstance = HttpApplicationFactory.GetApplicationInstance( context );

    if (applicationInstance is IHttpAsyncHandler)
    {
        IHttpAsyncHandler handler2 = (IHttpAsyncHandler) applicationInstance;
        context.AsyncAppHandler = handler2;
        handler2.BeginProcessRequest(context, this._handlerCompletionCallback, context);
        //HttpHandler异步处理请求
        handler2.BeginProcessRequest( context, this._handlerCompletionCallback, context );
    }
   else
   {
       //HttpHandler同步处理请求
       applicationInstance.ProcessRequest(context);
       this.FinishRequest(context.WorkerRequest, context, null);
   }
}

//HttpApplicationFactory.GetNormalApplicationInstance的部分源码
//此方法的作用:创建HttpApplication对象
//此方法内部维护着一个HttpApplication池,从池中取出一个闲置状态的HttpApplication或重新创建一个HttpApplication来处理Http请求
private HttpApplication GetNormalApplicationInstanceHttpContext context )
{
    HttpContext state = null;
    //如果HttpApplication池存在处于闲置状态的HttpApplication则取出一个用于处理请求
    if (this._numFreeAppInstances > 0)
    {
        state = (HttpApplication)this._freeList.Pop( );
        //更新HttpApplication计数器
        this._numFreeAppInstances--;
    }
    //如果池中没有闲置的HttpApplication,则重新创建一个HttpApplication
    else
    {
        state = (HttpApplication)HttpRuntime.CreateNonPublicInstance( this._theApplicationType );
    }
    //InitInternal方法内部会调用InitModules方法
    state.InitInternal( context, this._state, this._eventHandlerMethods );
    return state;
}

//HttpApplicationFactory.InitModules的部分源码
//此方法的作用:初始化Http模块(HttpModule)、发布管道事件
private void InitModules( )
{
    //读取注册在Web.config配置文件中的所有HttpModule节信息
    HttpModuleCollection modules = RuntimeConfig.GetAppConfig( ).HttpModules.CreateModules( );
    //HttpModule集合作为HttpApplicationFactory._moduleCollection的成员被使用
    this._moduleCollection = modules;
    //初始化所有HttpModule对象
    this.InitModulesCommon( );
    //发布处理Http请求的管道事件,当请求进入HttpApplication管道后就会逐个触发管道事件
    //而HttpModule因为订阅了管道事件,所以必然会得到执行,HttpHandler则在OnPostResolveRequestCache事件发生之后得到执行
    this._stepManager.BuildSteps( this._resumeStepsWaitCallback );
}

HttpRuntime(请求运行时)

主要作用就是创建一系列与处理Http请求有关系的对象。 

HttpApplication

HttpRuntime创建完与处理Http请求有关系的对象后最终会把Http请求交给HttpApplication对象,HttpApplication是处理http请求的最终管道,当一个请求进入Application管道后,处于管道中的程序就可以对请求作出处理,管道中都是一些插入式的Http处理模块和Http处理程序,你可以自定义处理模块和处理程序,然后在web.config中注册它们。当请求进入HttpApplication后,这些处理模块和处理程序会被自动读取并在初始化后得到执行。ASP.NET内置了一些处理模块和处理程序,将它们插入到了HttpApplication管道中,用于处理Http请求。为了处理特殊的Http请求,也可以自己实现ISAPI扩展和ISAPI过滤器,但不建议这样做,因为创建ISAPI扩展和ISAPI过滤器相当复杂。而微软提供了IHttpHandle替代ISAPI扩展,IHttpModule替代ISAPI过滤器,开发人员也可以自己创建IHttpHandle和IHttpModule接口的实现类来替代ISAPI扩展和ISAPI过滤器。

20个与Http请求有关的管道事件

HttpApplication定义了20个与Http请求有关的事件,任何Http请求一旦进入HttpApplication管道后就按照下面的事件的触发顺序依次执行。

BeginRequest
//开始处理请求时触发
AuthenticateRequest
//正在获取用户的身份标识时触发
PostAuthenticateRequest
//已建立用户的身份标识后触发
AuthorizeRequest
//正在从Cookie中获取用户的身份标识,以便验证该用户是否具有登录权限时触发
PostAuthorizeRequest
//验证已经通过后触发
OnResolveRequestCache
//正在获取页面的缓存信息时触发
PostResolveRequestCache
//已获取页面的缓存信息后触发
PostMapRequestHandler
//创建HttpHandle处理程序实例之后触发
AcquireRequestState
//取得Session时触发
PostAcquireRequestState
//取得Session之后触发
PreRequestHandlerExecute
//执行HttpHandle处理程序之前触发
PostRequestHandlerExecute
//执行完HttpHandle处理程序之后触发
ReleaseRequestState
//正在释放请求状态时触发
PostReleaseRequestState
//释放请求状态完成后触发
UpdateRequestCache
//更新缓存时触发
PostUpdateRequestCache
//更新缓存后触发
LogRequest
//正在将请求写入日志时触发
PostLogRequest
//已将请求写入日志后触发
Error
//处理请求期间发生任何错误时触发
EndRequest
//请求已经走完所有的Http模块和Http处理程序之后触发
View Code

HttpApplication的属性

Application
//应用程序的全局变量
Context
//返回一个HttpContext,HttpContext表示HTTP请求的上下文对象,它作为HttpApplication的Context 属性被存取。
Modules
//HttpModule模块集合
Request
//Http请求的参数值的对象
Response
//输出数据的对象
Server
//服务端信息对象
Session
//会话对象
User
//身份认证对象
View Code

ASP.NET中部分内置的处理模块和处理程序

<configuration>
     <system.web>
          <httpModules>
               <!--name:处理模块的类名,type:命名空间,所在程序集-->
               <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
          </httpModules>
          <httpHandlers>
               <!--name:处理程序的类名,type:命名空间,所在程序集-->
               <add verb="*" path=".config" type="System.Web.HttpForbiddenHandler"/>
               <add verb="*" path=".aspx" type="System.Web.UI.PageHandlerFactory"/>
          </httpHandlers>
     </system.web>
</configuration>
View Code

IIS7及其以后的版本更改了处理模块和处理程序的配置方式,因为IIS7的整合功能而把处理模块和处理程序移入了system.webServer配置节

<configuration>
     <system.webServer>
          <!--name:处理模块的类名,type:命名空间,所在程序集-->
          <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
          </modules>
          <handlers>
               <!--name:处理程序的类名,type:命名空间,所在程序集-->
               <add verb="*" path=".config" type="System.Web.HttpForbiddenHandler"/>
               <add verb="*" path=".aspx" type="System.Web.UI.PageHandlerFactory"/>
          </handlers>
     </system.webServer>
</configuration>
View Code

HttpHandler(System.Web)

要实现自定义的处理程序,需要定义一个从IHttpHandler派生的类型,然后将其注册到web.config文件中。当请求到达Application管道后,会进入自定义的处理程序。现在写一个简单的例子,实现对扩展名为.music的请求的处理程序:

namespace Users.Infrastructure
{
    public class MusicHandler : IHttpHandler
    {
        public bool IsReusable => true;

        public void ProcessRequest( HttpContext context )
        {
            //你的自定义处理Http请求的代码
            context.Response.Write( "<div>你的请求由MusicHandler负责处理</div>" );
        }
    }
}

注册处理程序

<system.webServer>
     <handlers>
          <!--扩展名为.music的请求由MessageHandle负责处理-->
          <add name="MusicHandler" verb="*" path="*.music" type="Users.Infrastructure.MusicHandler,Users"/>
     </handlers>
</system.webServer>

客户端发起请求:http://localhost:55880/xx.music,该请求会被MusicHandler截获,如果希望它可以截获任何http请求,则可以将以上配置信息中的path配置为:path='*'。

 

内置的HttpHandler

一般处理程序的扩展名为.ashx,该程序的类型就自动实现了IHttpHandler,但在web.config中看不到.net为.ashx程序注册的配置节,暂时不知道这种内置的一般处理程序注册在哪个文件中,不过可以想象应该是在machine.config中吧!如果不喜欢手动写特殊的自定义处理程序,可以考虑这种一般处理程序,但是如果希望任何请求都被截获,则可以手动实现自定义的处理程序。

HttpModule(System.Web)

HttpModule被称为处理模块是因为HttpApplication定义了20个非常重要的事件,而这些事件全部与Http请求有关系,当请求由HttpRunTime传递给HttpApplication后,这20个事件会依次序逐个被触发,而处理模块可以订阅这些事件以便当某个事件被触发时它可以及时截获请求并做出处理。要实现自定义的处理模块,需要定义一个从IHttpModule派生的类型,然后将其注册到web.config文件中。现在写一个简单的例子,实现对任何请求的处理的模块:

using System.Diagnostics;

namespace Users.Infrastructure
{
    public class MessageModule : IHttpModule
    {
        public static List<string> msgList = new List<string>( );
        private Stopwatch stopwatch = new Stopwatch( );
        public void Dispose( ) { }

        public void Init( HttpApplication context )
        {
            context.BeginRequest += new EventHandler( OnBeginRequest );
            context.AuthenticateRequest += new EventHandler( OnAuthenticateRequest );
            context.PostAuthenticateRequest += new EventHandler( OnPostAuthenticateRequest );
            context.AuthorizeRequest += new EventHandler( OnAuthorizeRequest );
            context.PostAuthorizeRequest += new EventHandler( OnPostAuthorizeRequest );
            context.ResolveRequestCache += new EventHandler( OnResolveRequestCache );
            context.PostResolveRequestCache += new EventHandler( OnPostResolveRequestCache );
            context.PostMapRequestHandler += new EventHandler( OnPostMapRequestHandler );
            context.PreRequestHandlerExecute += new EventHandler( OnPreRequestHandlerExecute );
            context.PostRequestHandlerExecute += new EventHandler( OnPostRequestHandlerExecute );
            context.AcquireRequestState += new EventHandler( OnAcquireRequestState );
            context.PostAcquireRequestState += new EventHandler( OnPostAcquireRequestState );
            context.ReleaseRequestState += new EventHandler( OnReleaseRequestState );
            context.PostReleaseRequestState += new EventHandler( OnPostReleaseRequestState );
            context.UpdateRequestCache += new EventHandler( OnUpdateRequestCache );
            context.PostUpdateRequestCache += new EventHandler( OnPostUpdateRequestCache );
            context.LogRequest += new EventHandler( OnLogRequest );
            context.PostLogRequest += new EventHandler( OnPostLogRequest );
            context.Error += new EventHandler( OnError );
            context.EndRequest += new EventHandler( OnEndRequest );            
        }

        public void OnBeginRequest( object sender, EventArgs e )
        {
            stopwatch.Start( );
            msgList.Add( $"BeginRequest: 开始处理请求时触发……" );
        }

        public void OnAuthenticateRequest( object sender, EventArgs e )
        {
            msgList.Add( $"AuthenticateRequest: 正在获取用户的身份标识时触发……" );
        }

        public void OnPostAuthenticateRequest( object sender, EventArgs e )
        {
            msgList.Add( $"PostAuthenticateRequest: 已建立用户的身份标识后触发…………" );
        }

        public void OnAuthorizeRequest( object sender, EventArgs e )
        {
            msgList.Add( $"AunthorizeRequest : 正在从Cookie中获取用户的身份标识,以便验证该用户是否具有登录权限时触发…………" );
        }

        public void OnPostAuthorizeRequest( object sender, EventArgs e )
        {
            msgList.Add( $"PostAuthorizeRequest : 验证已经通过后触发…………" );
        }

        public void OnResolveRequestCache( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 正在获取页面的缓存信息时触发…………" );
        }

        public void OnPostResolveRequestCache( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 已获取页面的缓存信息后触发…………" );
        }

        public void OnPostMapRequestHandler( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 创建HttpHandle处理程序实例之后触发…………" );
        }

        public void OnAcquireRequestState( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 取得Session时触发…………" );
        }

        public void OnPostAcquireRequestState( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 取得Session之后触发…………" );
        }

        //此事件处理完毕后,视图页面就会发送给客户端
        public void OnPreRequestHandlerExecute( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 执行HttpHandle处理程序之前触发…………" );
            msgList.Add( $"从请求发起到呈现视图的总耗时:{stopwatch.ElapsedMilliseconds}毫秒" );
        }

        //所以从这个事件起,纵然服务端继续运行以下事件,但客户端不会看到以下事件的输出结果
        public void OnPostRequestHandlerExecute( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 执行完HttpHandle处理程序之后触发…………" );
        }

        public void OnReleaseRequestState( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 正在释放请求状态时触发…………" );
        }

        public void OnPostReleaseRequestState( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 释放请求状态完成后触发…………" );
        }

        public void OnUpdateRequestCache( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 更新缓存时触发…………" );
        }

        public void OnPostUpdateRequestCache( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 更新缓存后触发…………" );
        }

        public void OnLogRequest( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 正在将请求写入日志时触发…………" );
        }

        public void OnPostLogRequest( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 已将请求写入日志后触发…………" );
        }

        public void OnError( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 处理请求发生错误时触发…………" );
        }

        public void OnEndRequest( object sender, EventArgs e )
        {
            msgList.Add( $"ResolveRequestCache : 请求已经走完所有的Http模块和Http处理程序之后触发…………" );
            msgList.Add( $"从请求发起到服务端释放资源总耗时:{stopwatch.ElapsedMilliseconds}毫秒" );
            stopwatch.Stop( );
        }
    }
}
View Code

注册处理模块

<system.webServer>
     <modules>
          <add name="MessageModule" type="Users.Infrastructure.MessageModule,Users"/>
          <!--name:模块的类名,type:命名空间,所在程序集-->
     </modules>
</system.webServer>
<h2>Index</h2>
欢迎 @HttpContext.Current.User.Identity.Name
@{ int counter = 0;}
<div>Http模块的执行顺序如下所示</div>
@foreach(string msg in MessageModule.msgList)
{
    counter++;    
    <div>@counter . @msg</div>
}

在客户端发起任何一个请求都会进入MessageModule处理模块,最终可以发现两个事实:

1.Http处理程序用于处理具体某一类扩展名的请求,而Http模块则会处理所有请求。

2.处理模块与处理程序谁先执行应视处理模块所订阅的事件而定,比如例子中的MessageModule订阅了20个事件,当从第1个事件执行到第11个事件后,HttpHandler已经被初始化(这个例子中我们没有注册HttpHandler而是执行了内置的HttpHandler)并开始执行,当HttpHandler执行完成(初始化控制器、调用Action方法、渲染视图并返回给客户端)后,MessageModule所订阅的事件还没执行完,它会接着做清理善后的工作。

 

ASP.NET MVC中的Http请求的处理机制

因为ASP.NET MVC使用了一套路由机制(定义在System.Web.Routing命名空间下的关于路由的类型),而IIS不能将客户端请求中的Url(路由没有扩展名)映射为处理程序,所以Http请求在ASP.NET MVC的HttpApplication管道中的处理流程与ASP.NET WebForms有一些区别,传统的aspx、ashx等还是由原来的System.Web.UI.PageHandlerFactory、HttpExtentions.SimpleHandler进行处理,而路由请求将由MVCHandler进行处理。为了能使MVCHandler处理Http请求,ASP.NET MVC做了一系列的工作,要用到的三个类型如下:

MVCRouteHandler(System.Web.MVC)

IRouteHandler接口的实现类,作用: 创建MvcHandler对象

MvcHandler(System.Web.MVC)

IHttpHandler接口的实现类,作用:处理Http请求。

UrlRoutingModule(System.Web.Routing) 

IHttpModule的实现类,作用:创建ControllerBuilder对象、获取MVCRouteHandler对象

路由请求能被解析为可执行的控制器其入口正是在UrlRoutingModule这个模块中实现的,UrlRoutingModule是ASP.NET MVC处理路由请求的标志性入口,这是因为UrlRoutingModule订阅了HttpApplication的PostResolveRequestCache事件,当一个Http请求进入管道并触发了PostResolveRequestCache事件后,UrlRoutingModule会响应这个事件,它将请求的Url与路由表中的路由进行匹配,然后获取一个与当前路由关联的MVCRouteHandler,通过MVCRouteHandler创建MvcHandler。

UrlRoutingModule的部分源码如下

public class UrlRoutingModule : IHttpModule
{
    public void Dispose( ) { }
    public void Init( HttpApplication context )
    {
        //PostResolveRequestCache事件发生时将由OnApplicationPostResolveRequestCache方法进行处理
        context.PostResolveRequestCache += new EventHandler( OnApplicationPostResolveRequestCache );
    }

    public void OnApplicationPostResolveRequestCache( object sender, EventArgs e )
    {
        //获取Http上下文对象
        HttpContextBase context = new HttpContextWrapper( ((HttpApplication)sender).Context );
        //调用PostResolveRequestCache方法
        PostResolveRequestCache( context );
    }

    public void PostResolveRequestCache( HttpContextBase context )
    {
      //创建一个ControllerBuilder对象存入静态Current属性中,这样可以使该对象成为内存中的全局唯一对象,可避免每次请求到达都重复创建相同的ControllerBuilder
      //ControllerBuilder用于创建控制器工厂,而控制器工厂用于实例化一个具体的与当前路由数据中的控制器名称对应的控制器
      ControllerBuilder.Current = new ControllerBuilder( );
        //将Http上下文对象作为参数传递给RouteTable.Routes.GetRouteData方法
        //该方法将从Http上下文对象中取出请求的Url信息
        //然后从路由表中查找与当前请求的Url相匹配的路由数据对象(RouteData)
        RouteData routeData = RouteTable.Routes.GetRouteData( context );
        if (routeData != null)
        {
            //每一个成功匹配了路由模板的Url请求都关联了一个MvcRouteHandler 
            //MvcRouteHandler通过RouteData.RouteHandler可以获取
            IRouteHandler routeHandler = routeData.RouteHandler;

            if (!(routeHandler is StopRoutingHandler))
            {            
                //MvcRouteHandler.GetHttpHandler方法会创建一个MvcHandler和一个ControllerBuilder对象
                IHttpHandler httpHandler = routeHandler.GetHttpHandler( requestContext );
                //把Http处理程序(MvcHandler)注入HttpApplication的处理管道中
                context.RemapHandler( httpHandler );
            }
        }
    }
}

 实现自定义的路由处理程序 

你也可以自定义一个Http模块和Http处理程序来处理ASP.NET MVC的路由请求,简单实现如下:

namespace MVC.Controllers
{
    public class MyRoutingModule : IHttpModule
    {
        public RouteCollection RouteCollection { get => RouteTable.Routes; }
        public void Dispose ( ) { }

        public void Init ( HttpApplication application )
        {
            application.PostResolveRequestCache += OnApplicationPostResolveRequestCache;
        }

        private void OnApplicationPostResolveRequestCache ( object sender , EventArgs e )
        {
            HttpContext contex = ( sender as HttpApplication ).Context;
            HttpContextBase contextWrapper = new HttpContextWrapper ( contex );
            RouteData routeData = RouteCollection.GetRouteData ( contextWrapper );

            RequestContext requestContext = new RequestContext
            {
                HttpContext = contextWrapper ,
                RouteData = routeData
            };
            routeData.RouteHandler = new MyRouteHandler ( );
            IHttpHandler handler = routeData.RouteHandler.GetHttpHandler ( requestContext ); 
            contex.RemapHandler ( handler ); 
        }
    }
    public class MyRouteHandler : IRouteHandler
    {
        public IHttpHandler GetHttpHandler ( RequestContext requestContext )
        {
            return new MyHttpHandler ( );
        }

        //嵌套类
        private class MyHttpHandler : IHttpHandler
        {
            public bool IsReusable => throw new NotImplementedException ( );

            public void ProcessRequest ( HttpContext context )
            {
                context.Response.Write ( "这是自定义的路由处理程序" );
            }
        }
    }
}
View Code

 注册处理模块

system.webServer>
    <modules>
       <add name="MyRoutingModule" type="MVC.Controllers.MyRoutingModule"/>
    </modules>
</system.webServer
View Code 

接下来就是解析MvcHandler如何激活控制器,当请求进入MvcHandler掌管后,它会调用自身的BeginProcessRequest方法,此方法将会解析RouteData中的Controller和Action,将对应的控制器激活以便调用控制器的方法来处理请求:ASP.NET MVC - 控制器

 

本文参考资料 

Edison Chou :ASP.Net请求处理机制初步探索之旅 

蒋金楠:《ASP.NET MVC5框架揭秘》之路由机制 

猴健居士 :AppDomain 

Leo_wlCnBlogsAppDomain解析 

shanhe理解AppDomain 

沉默的糕点AppDomain 动态加载及数据交互 

卡卡网:IIS里ISAPI扩展与ISAPI筛选器的区别和相同之处 

王乐WEB服务器5--IIS中ISAPI扩展、ISAPI筛选器

  

ASP.NET - 学习总目录

原文地址:https://www.cnblogs.com/myrocknroll/p/7615656.html