【源碼】進入ASP.NET MVC流程的大門 - UrlRoutingModule

UrlRoutingModule的功能

在ASP.NET MVC的請求過程當中,UrlRoutingModule的做用是攔截當前的請求URL,經過URL來解析出RouteData,爲後續的一系列流程提供所需的數據,好比ControllerAction等等。其實,UrlRoutingModule和咱們自定義的HttpModule都是實現了IHttpModule接口的兩個核心方法,Init方法和Dispose方法。下面是MVC中實現UrlRoutingModule代碼。首先,在初始化的方法中檢查該Module是否被加入到了當前請求的請求管道,而後註冊了管道事件中的PostResolveRequestCache事件。其實最理想的註冊事件應該是MapRequestHandler事件,可是爲了考慮到兼容性(IIS 6 和 IIS 7 ISAPI模式下不可用),微軟選擇了緊鄰MapRequestHandler事件以前的PostResolveRequestCache事件。web

 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作了一個封裝。HttpContextBaseHttpContextWrapper的基類,真正封裝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後還要繼續驗證是否是UrlAuthFailureHandlerUrlAuthFailureHandler也實現了IHttpHandler,噹噹前請求無權限時,用於返回401錯誤。app

 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 }

若是文中有表述不正確或有疑問的能夠在評論中指出,一塊兒學習一塊兒進步!!測試

相關文章
相關標籤/搜索