UrlRoutingModule的功能
在ASP.NET MVC的请求过程中,UrlRoutingModule
的作用是拦截当前的请求URL,通过URL来解析出RouteData
,为后续的一系列流程提供所需的数据,比如Controller
、Action
等等。其实,UrlRoutingModule
和我们自定义的HttpModule
都是实现了IHttpModule
接口的两个核心方法,Init
方法和Dispose
方法。下面是MVC中实现UrlRoutingModule
代码。首先,在初始化的方法中检查该Module是否被加入到了当前请求的请求管道,然后注册了管道事件中的PostResolveRequestCache
事件。其实最理想的注册事件应该是MapRequestHandler
事件,但是为了考虑到兼容性(IIS 6 和 IIS 7 ISAPI模式下不可用),微软选择了紧邻MapRequestHandler
事件之前的PostResolveRequestCache
事件。
1 protected virtual void Init(HttpApplication application) 2 { 3 // 检查 UrlRoutingModule 是否已经被加入到请求管道中 4 if (application.Context.Items[_contextKey] != null) 5 { 6 // 已经添加到请求管道则直接返回 7 return; 8 } 9 application.Context.Items[_contextKey] = _contextKey; 10 11 // 理想情况下, 我们应该注册 MapRequestHandler 事件。但是,MapRequestHandler事件在 12 // II6 或 IIS7 ISAPI 模式下是不可用的,所以我们注册在 MapRequestHandler 之前执行的事件, 13 // 该事件适用于所有的IIS版本。 14 application.PostResolveRequestCache += OnApplicationPostResolveRequestCache; 15 }
在注册事件中,将HttpApplication
的请求上下文HttpContext
做了一个封装,因为HttpContext
是没有基类的,也不是Virtual的,所以没办法做单元测试,也就是不可Mock的,所以针对HttpContext
做了一个封装。HttpContextBase
是HttpContextWrapper
的基类,真正封装HttpContext
的就是HttpContextWrapper
,所以三者之间的关系就是这样的。完成封装以后开始执行PostResolveRequestCache
方法,并将封装好的请求上下文作为参数传入。
1 private void OnApplicationPostResolveRequestCache(object sender, EventArgs e) 2 { 3 //HttpContextWrapper继承自HttpContextBase,用于替换HttpContext以实现可测试的接口 4 HttpApplication app = (HttpApplication)sender; 5 HttpContextBase context = new HttpContextWrapper(app.Context); 6 PostResolveRequestCache(context); 7 }
进入PostResolveRequestCache
事件后,UrlRoutingModule
开始真正的工作,该方法是处理URL的核心方法。根据当前请求的上下文,去匹配路由表是否存在与之匹配的URL,如果匹配则从路由信息中获取RouteData
,以及IRouteHandler
。拿到IRouteHandler
后,要进行一些列的判断,比如要判断是否是StopRoutingHandler
,在Global.asax文件中有一段RouteConfig.RegisterRoutes(RouteTable.Routes);
代码,在这个RegisterRoutes
方法中有一句routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
表示需要过滤掉的路由,而这个IgnoreRoute
路由的Handler就是StopRoutingHandler
,所以在这里做了判断,Handler是StopRoutingHandler
则不往下执行,直接return,不再处理这条请求,如果是则路由模块会停止继续处理请求,如果不是则继续处理,并根据RequestContext
来获取IHttpHandler
,拿到IHttpHandler
后还要继续验证是不是UrlAuthFailureHandler
,UrlAuthFailureHandler
也实现了IHttpHandler
,当当前请求无权限时,用于返回401错误。
1 public virtual void PostResolveRequestCache(HttpContextBase context) 2 { 3 // 匹配传入的URL,检查路由表中是否存在与之匹配的URL 4 RouteData routeData = RouteCollection.GetRouteData(context); 5 6 // 如果没有找到匹配的路由信息,直接返回 7 if (routeData == null) 8 { 9 return; 10 } 11 12 // 如果找到的匹配的路由,则从路由信息的RouteHandler中获取IHttpHandler 13 IRouteHandler routeHandler = routeData.RouteHandler; 14 if (routeHandler == null) 15 { 16 throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.UrlRoutingModule_NoRouteHandler))); 17 } 18 19 // 如果该IRouteHandler是StopRoutingHandler,路由模块会停止继续处理该请求 20 // routes and to let the fallback handler handle the request. 21 if (routeHandler is StopRoutingHandler) 22 { 23 return; 24 } 25 26 RequestContext requestContext = new RequestContext(context, routeData); 27 28 // 将路由信息添加到请求上下文 29 context.Request.RequestContext = requestContext; 30 31 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); 32 if (httpHandler == null) 33 { 34 throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, SR.GetString(SR.UrlRoutingModule_NoHttpHandler), routeHandler.GetType())); 35 } 36 37 // 如果该IHttpHandler是认证失败的IHttpHandler,返回401权限不足错误 38 if (httpHandler is UrlAuthFailureHandler) 39 { 40 if (FormsAuthenticationModule.FormsAuthRequired) 41 { 42 UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this); 43 return; 44 } 45 else 46 { 47 throw new HttpException(401, SR.GetString(SR.Assess_Denied_Description3)); 48 } 49 } 50 51 // Remap IIS7 to our handler 52 context.RemapHandler(httpHandler); 53 }
如果请求认证失败,返回401错误,并且调用CompleteRequest
方法,显式地完成当前请求。
1 internal static void ReportUrlAuthorizationFailure(HttpContext context, object webEventSource) 2 { 3 // 拒绝访问 4 context.Response.StatusCode = 401; 5 WriteErrorMessage(context); 6 7 if (context.User != null && context.User.Identity.IsAuthenticated) { 8 // 这里AuditUrlAuthorizationFailure指示在Web请求过程中URL授权失败的事件代码 9 WebBaseEvent.RaiseSystemEvent(webEventSource, WebEventCodes.AuditUrlAuthorizationFailure); 10 } 11 context.ApplicationInstance.CompleteRequest(); 12 }
方法GetRouteData
的作用是根据当前请求的上下文来获取路由数据,在匹配RouteCollection
集合之前,会检查当前的请求是否是静态文件,如果请求的是存在于服务器上的静态文件则直接返回,否则继续处理当前请求。
1 public RouteData GetRouteData(HttpContextBase httpContext) 2 { 3 if (httpContext == null) 4 { 5 throw new ArgumentNullException("httpContext"); 6 } 7 if (httpContext.Request == null) 8 { 9 throw new ArgumentException(SR.GetString(SR.RouteTable_ContextMissingRequest), "httpContext"); 10 } 11 12 // Optimize performance when the route collection is empty.当路由集合是空的的时候优化性能. The main improvement is that we avoid taking 13 // a read lock when the collection is empty.主要的改进是当集合为空的时候避免添加只读锁。 Without this check, the UrlRoutingModule causes a 25%-50% 14 // 没有这个检查的话,UrlRoutingModule 性能会因为锁的缘故而下降25%-50% 15 // regression in HelloWorld RPS due to lock contention. The UrlRoutingModule is now in the root web.config, 16 // UrlRoutingModule目前被配置在根目录的web.config 17 // so we need to ensure the module is performant, especially when you are not using routing. 18 // 所以我们应该确认下这个module是否是高效的,尤其是当没有使用路由的时候。 19 // This check does introduce a slight bug, in that if a writer clears the collection as part of a write 20 // transaction, a reader may see the collection when it's empty, which the read lock is supposed to prevent. 21 // We will investigate a better fix in Dev10 Beta2. The Beta1 bug is Dev10 652986. 22 if (Count == 0) { 23 return null; 24 } 25 26 bool isRouteToExistingFile = false; 27 // 这里只检查一次 28 bool doneRouteCheck = false; 29 if (!RouteExistingFiles) 30 { 31 isRouteToExistingFile = IsRouteToExistingFile(httpContext); 32 doneRouteCheck = true; 33 if (isRouteToExistingFile) 34 { 35 // If we're not routing existing files and the file exists, we stop processing routes 36 // 如果文件存在,但是路由并没有匹配上,则停止继续处理当前请求。 37 return null; 38 } 39 } 40 41 // Go through all the configured routes and find the first one that returns a match 42 // 遍历所有已配置的路由并且返回第一个与之匹配的 43 using (GetReadLock()) 44 { 45 foreach (RouteBase route in this) 46 { 47 RouteData routeData = route.GetRouteData(httpContext); 48 if (routeData != null) 49 { 50 // If we're not routing existing files on this route and the file exists, we also stop processing routes 51 if (!route.RouteExistingFiles) 52 { 53 if (!doneRouteCheck) 54 { 55 isRouteToExistingFile = IsRouteToExistingFile(httpContext); 56 doneRouteCheck = true; 57 } 58 if (isRouteToExistingFile) 59 { 60 return null; 61 } 62 } 63 return routeData; 64 } 65 } 66 } 67 return null; 68 }
下面这段代码就是获取相对路径来检测文件夹和文件是否存在,存在返回true
,否则返回false
。
1 // 如果当前请求的是一个存在的文件,则返回true 2 private bool IsRouteToExistingFile(HttpContextBase httpContext) 3 { 4 string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath; 5 return ((requestPath != "~/") && 6 (VPP != null) && 7 (VPP.FileExists(requestPath) || 8 VPP.DirectoryExists(requestPath))); 9 }
如果文中有表述不正确或有疑问的可以在评论中指出,一起学习一起进步!!