MVC之前的那點事兒系列(8):UrlRouting的理解

湯姆大叔發表於2014-06-09

文章內容

根據對Http Runtime和Http Pipeline的分析,我們知道一個ASP.NET應用程式可以有多個HttpModuel,但是隻能有一個HttpHandler,並且通過這個HttpHandler的BeginProcessRequest(或ProcessRequest)來處理並返回請求,前面的章節將到了再MapHttpHandler這個週期將會根據請求的URL來查詢對應的HttpHandler,那麼它是如何查詢的呢?

一起我們在做自定義HttpHandler的時候,需要執行URL以及副檔名匹配規則,然後查詢HttpHandler的時候就是根據相應的規則來查詢哪個HttpHandler可以使用。另一方面我們本系列教材講的MVC就是通過註冊路由(Route)來匹配到對應的Controller和Action上的,例如Global.asax裡的程式碼:

routes.MapRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = UrlParameter.Optional }

但是在匹配這個之前,MVC首先要接管請求才能處理,也就是說我們要有對應MVC的HttpHandler(後面知道它的名字叫MvcHandler)被MapRequestHandler週期的處理引擎查詢到並且應用上才行,然後後面才能由 Controller/Action執行。另外一方面,由於該URL地址沒有副檔名,所以無法進入ASP.NET的RunTime,MVC2的實現方式是:註冊萬用字元(*.*)對映到aspnet_ISPAI.dll,然後通過一個自定義的UrlRoutingModuel來匹配Route規則,再繼續處理,但是MVC3的時候,匹配Route規則的處理機制整合到ASP.NET4.0裡了,也就是今天我們這篇文章所要講的主角(UrlRoutingModule)的處理機制。

 

先來看UrlRoutingModule的原始碼,無容置疑地這個類是繼承於IHttpModule,首先看一下Init方法的程式碼:

protected virtual void Init(HttpApplication application) {

    ////////////////////////////////////////////////////////////////// 
    // Check if this module has been already addded
    if (application.Context.Items[_contextKey] != null) { 
        return; // already added to the pipeline 
    }
    application.Context.Items[_contextKey] = _contextKey; 

    // Ideally we would use the MapRequestHandler event.  However, MapRequestHandler is not available
    // in II6 or IIS7 ISAPI Mode.  Instead, we use PostResolveRequestCache, which is the event immediately
    // before MapRequestHandler.  This allows use to use one common codepath for all versions of IIS. 
    application.PostResolveRequestCache += OnApplicationPostResolveRequestCache;
}

該程式碼在PostResolveRequestCache週期事件上新增了我們需要執行的方法,用於URL匹配規則的設定,但是為什麼要在這個週期點上新增事件呢?看了註釋,再結合我們前面對Pipeline的瞭解,釋然了,要像動態註冊自己的HttpHandler,那就需要在MapRequestHandler之前進行註冊自己的規則(因為這個週期點就是做這個事情的),但由於IIS6不支援這個事件,所以為了能讓IIS6也能執行MVC3,所以我們需要在這個週期之前的PostResolveRequestCache的事件點上去註冊我們的規則,也許如果IIS6被微軟廢棄以後,就會將這個事件新增到真正的開始點MapRequestHandler上哦。

 

我們繼續來看註冊該事件的OnApplicationPostResolveRequestCache方法的程式碼:

public virtual void PostResolveRequestCache(HttpContextBase context) { 
    // Match the incoming URL against the route table
    RouteData routeData = RouteCollection.GetRouteData(context);

    // Do nothing if no route found 
    if (routeData == null) {
        return; 
    } 

    // If a route was found, get an IHttpHandler from the route's RouteHandler 
    IRouteHandler routeHandler = routeData.RouteHandler;
    if (routeHandler == null) {
        throw new InvalidOperationException(
            String.Format( 
                CultureInfo.CurrentUICulture,
                SR.GetString(SR.UrlRoutingModule_NoRouteHandler))); 
    } 

    // This is a special IRouteHandler that tells the routing module to stop processing 
    // routes and to let the fallback handler handle the request.
    if (routeHandler is StopRoutingHandler) {
        return;
    } 

    RequestContext requestContext = new RequestContext(context, routeData); 
 
    // Dev10 766875    Adding RouteData to HttpContext
    context.Request.RequestContext = requestContext; 

    IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
    if (httpHandler == null) {
        throw new InvalidOperationException( 
            String.Format(
                CultureInfo.CurrentUICulture, 
                SR.GetString(SR.UrlRoutingModule_NoHttpHandler), 
                routeHandler.GetType()));
    } 

    if (httpHandler is UrlAuthFailureHandler) {
        if (FormsAuthenticationModule.FormsAuthRequired) {
            UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this); 
            return;
        } 
        else { 
            throw new HttpException(401, SR.GetString(SR.Assess_Denied_Description3));
        } 
    }

    // Remap IIS7 to our handler
    context.RemapHandler(httpHandler); 
}

我已經加粗了4行重要的程式碼,第一行是通過傳遞HttpContext引數,從RouteCollection找到對應的靜態屬性RouteData( GetRouteData方法裡會先判斷真實檔案是否存在,如果不存在才去找RouteData),第二行然後從RouteData的屬性RouteHandler獲取一個IRouteHandler的例項,第三行是從該例項裡獲取對應的IHttpHandler例項,第4行是呼叫HttpContext的RemapHandler方法重新map新的handler(這行程式碼的註釋雖然說是remap IIS7,其實IIS6也是用了,只不過判斷該方法裡對IIS7整合模式多了一點特殊處理而已),然後可以通過HttpContext. RemapHandlerInstance屬性來得到這個例項。

關於Route/RouteData/RouteCollection/IRouteHandler的作用主要就是定義URL匹配到指定的IHttpHandler,然後註冊進去,具體實現我們稍後再講,現在先看一下Http Pipeline裡是如何找到這個IHttpHandler例項的,由於IIS6和IIS7整合模式是差不多的,前面的文章我們提到了都是最終呼叫到IHttpHandlerFactory的例項,然後從中獲取IHttpHandler,所以我們這裡只分析IIS6和IIS7經典模式的實現。

 

先來看BuildSteps裡查詢HttpHandler的方法MapHandlerExecutionStep的程式碼,只有幾行程式碼,最重要的是:

context.Handler = _application.MapHttpHandler(
    context,
    request.RequestType,
    request.FilePathObject, 
    request.PhysicalPathInternal,
    false /*useAppConfig*/); 

MapHttpHandler就是我們要查詢Handler的方法了,來仔細看看程式碼:

internal IHttpHandler MapHttpHandler(HttpContext context, String requestType, VirtualPath path, String pathTranslated, bool useAppConfig) { 
    // Don't use remap handler when HttpServerUtility.Execute called
    IHttpHandler handler = (context.ServerExecuteDepth == 0) ? context.RemapHandlerInstance : null;

    using (new ApplicationImpersonationContext()) { 
        // Use remap handler if possible
        if (handler != null){ 
            return handler; 
        }
 
        // Map new handler
        HttpHandlerAction mapping = GetHandlerMapping(context, requestType, path, useAppConfig);

        // If a page developer has removed the default mappings with <httpHandlers><clear> 
        // without replacing them then we need to give a more descriptive error than
        // a null parameter exception. 
        if (mapping == null) { 
            PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_NOT_FOUND);
            PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_FAILED); 
            throw new HttpException(SR.GetString(SR.Http_handler_not_found_for_request_type, requestType));
        }

        // Get factory from the mapping 
        IHttpHandlerFactory factory = GetFactory(mapping);
 
 
        // Get factory from the mapping
        try { 
            // Check if it supports the more efficient GetHandler call that can avoid
            // a VirtualPath object creation.
            IHttpHandlerFactory2 factory2 = factory as IHttpHandlerFactory2;
 
            if (factory2 != null) {
                handler = factory2.GetHandler(context, requestType, path, pathTranslated); 
            } 
            else {
                handler = factory.GetHandler(context, requestType, path.VirtualPathString, pathTranslated); 
            }
        }
        catch (FileNotFoundException e) {
            if (HttpRuntime.HasPathDiscoveryPermission(pathTranslated)) 
                throw new HttpException(404, null, e);
            else 
                throw new HttpException(404, null); 
        }
        catch (DirectoryNotFoundException e) { 
            if (HttpRuntime.HasPathDiscoveryPermission(pathTranslated))
                throw new HttpException(404, null, e);
            else
                throw new HttpException(404, null); 
        }
        catch (PathTooLongException e) { 
            if (HttpRuntime.HasPathDiscoveryPermission(pathTranslated)) 
                throw new HttpException(414, null, e);
            else 
                throw new HttpException(414, null);
        }

        // Remember for recycling 
        if (_handlerRecycleList == null)
            _handlerRecycleList = new ArrayList(); 
        _handlerRecycleList.Add(new HandlerWithFactory(handler, factory)); 
    }
 
    return handler;
}

從程式碼可以看出,首先如果當前頁面使用了HttpServerUtility.Execute進行頁面內跳轉,就不使用我們通過路由設定的HttpHandler(也就是HttpContent.RemapHandlerInstance屬性),如果沒有跳轉,就使用,並且優先順序是第一的,只有當不設定任何基於Route的HttpHandler,才走剩餘的匹配規則(也就是之前ASP.NET預設的按照副檔名類匹配的,這部分和我們關係不大就不做詳細分析了)。

 

好了,知道了UrlRouteModuel的大概機制,我們再回頭看看如何通過Route/RouteData/RouteCollection/IRouteHandler這幾個類來實現動態註冊Route規則的,先來看Route的程式碼:

[TypeForwardedFrom("System.Web.Routing, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")]
public class Route : RouteBase
{    
    public Route(string url, IRouteHandler routeHandler)
    {
        Url = url;
        RouteHandler = routeHandler;
    }
     
    public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) { 
            Url = url;
            Defaults = defaults; 
            Constraints = constraints; 
            RouteHandler = routeHandler;
        }

    //省略部分程式碼
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        // Parse incoming URL (we trim off the first two chars since they're always "~/")
        string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;

        RouteValueDictionary values = _parsedRoute.Match(requestPath, Defaults);

        if (values == null)
        {
            // If we got back a null value set, that means the URL did not match
            return null;
        }

        RouteData routeData = new RouteData(this, RouteHandler);

                // Validate the values
        if (!ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest)) { 
            return null; 
        }
 
        // Copy the matched values
        foreach (var value in values) {
            routeData.Values.Add(value.Key, value.Value);
        } 

        // Copy the DataTokens from the Route to the RouteData 
        if (DataTokens != null) { 
            foreach (var prop in DataTokens) {
                routeData.DataTokens[prop.Key] = prop.Value; 
            }
        }
        return routeData;
    }       
    }

Route程式碼提供了一系列的建構函式過載(我們這裡只列出了兩個),建構函式主要是傳入URL和對應的IRouteHandler例項以及約束規則(比如正則等),然後提供了一個最重要的GetRouteData方法,用於將Route自身和IRouteHandler組裝成RouteData,然後返回(中途也會驗證相應的約束條件,比如是否符合某個正規表示式),RouteData類本身沒有什麼邏輯,只是暴露了Route和RouteHandler屬性。

 

我們再來看RouteCollection,該類儲存了所有的Route規則(即URL和對應的IRouteHandler),通過靜態屬性RouteTable.Routes來獲取RouteCollection例項,通過UrlRoutingModule裡暴露的RouteCollection屬性我們可以驗證這一點:

public RouteCollection RouteCollection {
    get { 
        if (_routeCollection == null) { 
            _routeCollection = RouteTable.Routes;
        } 
        return _routeCollection;
    }
    set {
        _routeCollection = value; 
    }
} 

還有一個需要注意的,RouteHandler繼承的IRouteHandler的程式碼:

public interface IRouteHandler
{
     IHttpHandler GetHttpHandler(RequestContext requestContext);
}

該程式碼只提供了一個GetHttpHandler方法,所有實現這個介面的類需要實現這個方法,MVCHandler就是這麼實現的(下一章節我們再細看)。

 

至此,我們應該有一個清晰的認識了,我們通過全域性靜態屬性集合(RouteTable.Routes)去新增各種各樣的Route(但應該在HttpModule初始化週期之前),然後通過UrlRoutingModule負責註冊Route以及對應的IRouteHandler例項(IRouteHandler例項可以通過GetHttpHandler獲取IHttpHandler),最終實現根據不同URL來接管不同的HttpHandler。

 

MVC正是利用HttpApplication建立的週期(Application_Start方法)來新增了我們所需要的Route規則,當然在新增規則的時候帶上了MVCHandler這個重要的HttpHandler,

程式碼如下:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute(
        "Default",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = UrlParameter.Optional }
                );
}

MapRoute方法是一個擴充套件方法,通過該擴充套件方法註冊Route是個不錯的方法,下一章節,我們講講解MVC是如何註冊自己的MVCRouteHandler例項以及如何實現MVCHandler的呼叫的。

參考資料:

http://www.cnblogs.com/me-sa/archive/2009/06/01/MVCLifecycle.html

http://www.cnblogs.com/zhaoyang/archive/2011/11/16/2251200.html

同步與推薦

本文已同步至目錄索引:MVC之前的那點事兒系列

MVC之前的那點事兒系列文章,包括了原創,翻譯,轉載等各型別的文章,如果對你有用,請推薦支援一把,給大叔寫作的動力。

相關文章