2014年7月10日 星期四

C# - RouteHandler & IHttpHandler & PageRouteHandler & MvcRouteHandler解析和運用

ASP.Net MVC路由的運用,如何找到 IHttpHandler

在MVC中一個請求最終通過一個具體的HttpHandler進行處理,用於表示一個Web頁面的Page物件就是一個HttpHandler,被用於處理基於某個.aspx檔的請求。通過HttpHandler的動態映射來實現請求位址與物理檔路徑之間的分離。實際上ASP.NET路由系統就是採用了這樣的實現原理。ASP.NET路由系統通過一個註冊到當前應用的自訂HttpModule對所有的請求進行攔截,並通過對請求的分析為之動態匹配一個用於處理它的HttpHandler。HttpHandler對請求進行處理後將相應的結果寫入HTTP回復以實現對請求的相應,如下圖所示:



請求攔截器的HttpModule類型為UrlRoutingModule。UrlRoutingModule對請求的攔截是通過註冊表示當前應用的HttpApplication的PostResolveRequestCache事件實現的,看下面的代碼:

public class UrlRoutingModule : IHttpModule
{
    public RouteCollection RouteCollection { get; set; }
    public void Init(HttpApplication context)
    {
        context.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache);
       
    }
    private void OnApplicationPostResolveRequestCache(object sender,  EventArgs e);

}

UrlRoutingModule具有一個類型為RouteCollection的RouteCollection屬性,在預設的情況下引用這通過RouteTable的靜態屬性Routes表示的全域路由表。針對請求的HttpHandler的動態映射就實現在OnApplicationPostResolveRequestCache方法中,具體的實現邏輯非常簡單:通過HttpApplication獲得但前的HTTP上下文,並將其作為參數調用RouteCollection的GetRouteData方法得到一個RouteData物件。

通過RouteData的RouteHandler屬性可以得到一個IRouteHandler的路由處理器物件,而調用後者的GetHttpHandler方法直接可以取得對應的HttpHandler物件,需要Reflection(映射)到當前請求的就是這麼一個 HttpHandler。下面的程式碼呈現了定義在UrlRoutingModule的OnApplicationPostResolveRequestCache方法中的動態HttpHandler映射邏輯。

public class UrlRoutingModule : IHttpModule
{
    private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
    {
        HttpContext context = ((HttpApplication)sender).Context;
        HttpContextBase contextWrapper = new HttpContextWrapper(context);
        RouteData routeData = this.RouteCollection.GetRouteData(contextWrapper);
        RequestContext requestContext = new RequestContext(contextWrapper, routeData);
        IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
        context.RemapHandler(handler);
        }
}

PageRouteHandler & MvcRouteHandler

前面的說明對於使用RouteCollection的GetRouteData取得的RouteData物件,其RouteHandler來源於配對的Route對象。對於通過調用RouteCollection的MapPageRoute方法註冊的Route來說,它的RouteHandler是一個類型為PageRouteHandler對象。
由於調用MapPageRoute方法的目的在於實現請求http address位址與某個.aspx分頁檔之間的映射,最終還是要創建的Page物件處理相應的請求,所以PageRouteHandler的GetHttpHandler方法最終返回的就是針對映射分頁檔路徑的Page物件。此外,MapPageRoute方法中還可以控制是否對物理檔位址實施授權,而授權在返回Page物件之前進行。
定義在PageRouteHandler中的HttpHandler獲取邏輯基本上體現在如下的代碼片斷中,兩個屬性VirtualPath和CheckPhysicalUrlAccess表示分頁檔的位址和是否需要對物理檔位址實施URL授權,它們在構造函數中被初始化(Initialization),而最終的來源從調用RouteCollection的MapPageRoute方法傳入的參數。

public class PageRouteHandlerIRouteHandler
{
    public bool CheckPhysicalUrlAccess { get; private set; }
    public string VirtualPath { get; private set; }
    public PageRouteHandler(string virtualPath, bool checkPhysicalUrlAccess)
    {
        this.VirtualPath = virtualPath;
        this.CheckPhysicalUrlAccess = checkPhysicalUrlAccess;
    }
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        if (this.CheckPhysicalUrlAccess)
        {
            //確認URL的存取,在這可任意處理
        }
        return (IHttpHandler)BuildManger.CreateInstanceFromVirtualPath(this.VirtualPath, typeof(Page));
    }
}


ASP.NET MVC的Route物件是通過調用RouteCollection的擴展方法MapRoute方法進行註冊的,它對應的RouteHandler是一個類型為MvcRouteHandler的對象。MvcRouteHandler用於獲取處理當前請求的HttpHandler是一個MvcHandler對象。MvcHandler實現對Controller的啟動、Action方法的執行以及對請求的相應,整個MVC框架實現在MvcHandler之中,如下面的程式所示:

public class MvcRouteHandlerIRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new MvcHandler(requestContext);
    }
}

ASP.NET路由擴展

大致來說,整個路由系統是通過對HttpHandler的動態註冊的方式來實現的。具體來說,UrlRoutingModule通過對代表Web應用的HttpApplication的PostResolveRequestCache事件的註冊實現了對於請求的攔截。對於被攔截的請求,UrlRoutingModule利用註冊的路由表對其進行配對和解析,然後得到包含所有路由資訊的RouteData物件。最終藉由該物件的RouteHandler創建出相應的HttpHandler映射到當前請求。從可擴展性的角度可以用三種方式來實現路由方式:

通過集成抽象類別RouteBase創建自訂Route定義路由邏輯。
通過實現介面IRouteHandler創建自訂RouteHandler定制HttpHandler提供機制。
通過實現IHttpHandler創建自訂HttpHandler來對請求處理。

通過自訂Route對ASP.NET路由的擴展

定義在ASP.NET路由系統中預設的路由類型Route建立了定義成文本範本的URL模式與某個物理檔之間的映射,如果我們對WCF REST有一定了解的話,具體來說,WCF REST也可以借助於System.UriTemplate這個物件實現了同樣定義成某個文本範本的URI模式與目標操作之間的映射。
我們創建一個新的ASP.NET Web應用,並且添加針對程式集System.ServiceModel.dll的引用(UriTemplate定義在該程式集中),然後創建如下一個針對UriTemplate的路由類型UriTemplateRoute。

public class UriTemplateRoute:RouteBase
{
    public UriTemplate UriTemplate { get; private set; }
    public IRouteHandler RouteHandler { get; private set; }
    public RouteValueDictionary DataTokens { get; private set; }

    public UriTemplateRoute(string template, string physicalPath, object dataTokens = null)
    {
        this.UriTemplate = new UriTemplate(template);
        this.RouteHandler = new PageRouteHandler(physicalPath);
        if (null != dataTokens)
        {
            this.DataTokens = new RouteValueDictionary(dataTokens);
        }
        else
        {
            this.DataTokens = new RouteValueDictionary();
        }
    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        Uri uri = httpContext.Request.Url;
        Uri baseAddress = new Uri(string.Format("{0}://{1}", uri.Scheme, uri.Authority));
        UriTemplateMatch match = this.UriTemplate.Match(baseAddress, uri);
        if (null == match)
        {
            return null;
        }

        RouteData routeData = new RouteData();
        routeData.RouteHandler = this.RouteHandler;
        routeData.Route = this;
        foreach (string name in match.BoundVariables.Keys)
        {
            routeData.Values.Add(name,match.BoundVariables[name]);
        }

        foreach (var token in this.DataTokens)
        {
            routeData.DataTokens.Add(token.Key, token.Value);
        }

        return routeData;
    }

    public override VirtualPathData GetVirtualPath(VRequestContext requestContext, RouteValueDictionary values)
    {
        Uri uri = requestContext.HttpContext.Request.Url;
        Uri baseAddress = new Uri(string.Format("{0}://{1}", uri.Scheme, uri.Authority));
        Dictionary<string, string> variables = new Dictionary<string, string>();
        foreach(var item in values)
        {
            variables.Add(item.Key, item.Value.ToString());
        }
   
        //確定路徑變數是否被提供
        foreach (var name in this.UriTemplate.PathSegmentVariableNames)
        {
            if(!this.UriTemplate.Defaults.Keys.Any(key=> string.Compare(name,key,true) == 0) &&
            !values.Keys.Any(key=> string.Compare(name,key,true) == 0))
            {
                return null;
            }
        }

        //確定查詢變數是否被提供
        foreach (var name in this.UriTemplate.QueryValueVariableNames)
        {
            if(!this.UriTemplate.Defaults.Keys.Any(key=> string.Compare(name,key,true) == 0) &&
            !values.Keys.Any(key=> string.Compare(name,key,true) == 0))
            {
                return null;
            }
        }

        Uri virtualPath = this.UriTemplate.BindByName(baseAddress, variables);
        string strVirtualPath = virtualPath.ToString().ToLower().Replace(baseAddress.ToString().ToLower(),"");
        VirtualPathData virtualPathData =  new VirtualPathData(this, strVirtualPath);
        foreach (var token in this.DataTokens)
        {
            virtualPathData.DataTokens.Add(token.Key, token.Value);
        }
            return virtualPathData;
    }
}

UriTemplateRoute具有UriTemplate、DataTokens和RouteHandler三個唯讀屬性,前兩個通過建構函數的參數進行初始化,後者則是創建PageRouteHandler物件。
用於請求進行匹配判斷的GetRouteData方法中,可以解析出基於應用的基底位址並請求位址作為參數調用UriTemplate的Match方法,如果返回的UriTemplateMatch物件不為Null,則表示URL範本的模式與請求位址配對。在配對的情況下創建並返回相應的RouteData物件,否則直接返回Null。
用於生成URL的GetVirtualPath方法中,通過定義在URL中的範本(包括變數名包含在屬性PathSegmentVariableNames的路徑段變數和包含在QueryValueVariableNames屬性的查詢變數)是否提供RouteValueDictionary欄位或者預設變數清單(通過屬性Defaults表示)來判斷URL是否與提供的變數清單配對。在配對的情況下通過調用UriTemplate的BindByName方法得到一個完整的Uri。由於該方法返回的是相對路徑,所以需要將應用基底位址剔除,最後創建並返回一個VirtualPathData物件。如果沒有配對,則直接返回Null。
在創建的Global.asax檔中採用如下的代碼對自訂的UriTemplateRoute進行註冊,基於UriTemplate的URI範本比針對Route的URL範本,我們直接將預設值定義在範本,讓它在定義預設值方法更為直接,如下面的範例所示:

public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        UriTemplateRoute route = new UriTemplateRoute("{areacode=010}/{days=2}",
        "~/Weather.aspx", new { defualtCity = "BeiJing", defaultDays = 2});
        RouteTable.Routes.Add("default", route);
    }
}

在註冊的路由對應的目標頁面Weather.aspx程式裡,定義了GenerateUrl根據指定的區號(areacode)和預報天數(days)創建一個Url,而Url的產生直接通過調用RouteTable的Routes屬性的GetVirtualPathData方法完成。生成的URL連同當前頁面的RouteData的屬性產生HTML,如下所示:




接觸MVC開發時一定會接觸到路由,那麼路由這東西的原理是如何運作呢?在web.config中有一段是這樣的:

<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />

這一段就是路由負責的DLL組件。在這個dll中有一個很特殊的類別UrlRoutingModule,它裏面主要的核心代碼:

protected virtual void Init(HttpApplication application)
{
    if (application.Context.Items[_contextKey] == null)
    {
        application.Context.Items[_contextKey] = _contextKey;
        application.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache);
    }
}

private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
{
    HttpContextBase context = new HttpContextWrapper(((HttpApplication) sender).Context);
    this.PostResolveRequestCache(context);
}

public virtual void PostResolveRequestCache(HttpContextBase context)
{
    RouteData routeData = this.RouteCollection.GetRouteData(context);
    if (routeData != null)
    {
        IRouteHandler routeHandler = routeData.RouteHandler;
        if (routeHandler == null)
        {
            throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.GetString("UrlRoutingModule_NoRouteHandler"), new object[0]));
        }
        if (!(routeHandler is StopRoutingHandler))
        {
            RequestContext requestContext = new RequestContext(context, routeData);
            context.Request.RequestContext = requestContext;
            IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
            if (httpHandler == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.GetString("UrlRoutingModule_NoHttpHandler"), new object[] { routeHandler.GetType() }));
            }
            if (httpHandler is UrlAuthFailureHandler)
            {
                if (!FormsAuthenticationModule.FormsAuthRequired)
                {
                    throw new HttpExcepion(0x191, SR.GetString("Assess_Denied_Description3"));
                }
                UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this);
            }
            else
            {
                context.RemapHandler(httpHandler);
            }
        }
    }

}

在IHttpModule.Init中註冊了一個PostResolveRequestCache事件,而該事件主要是調用PostResolveRequestCache這個方法:

RouteData routeData = this.RouteCollection.GetRouteData(context);
IRouteHandler routeHandler = routeData.RouteHandler;
RequestContext requestContext = new RequestContext(context, routeData);
context.Request.RequestContext = requestContext;
IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);

context.RemapHandler(httpHandler);

分析RouteData routeData = this.RouteCollection.GetRouteData(context) ,這句可以猜測是取得路由訊息。要想理解這句代碼又得回到程式中來,在Global.asax.cs檔中的RegisterRoutes方法 中,默認有一段程式是這樣:

routes.MapRoute(
    "Default", // 路由名稱
    "{controller}/{action}/{id}", // 帶有参數的 URL
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // 参數默認值

);

這句代碼主要是註冊一個路由,url不能隨便寫,需要有controller和action。實現的程式碼如下:

public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) {
    Route route = new Route(url, new MvcRouteHandler()) {
        Defaults = new RouteValueDictionary(defaults),
        Constraints = new RouteValueDictionary(constraints),
        DataTokens = new RouteValueDictionary()
    };

    if ((namespaces != null) && (namespaces.Length > 0)) {
        route.DataTokens["Namespaces"] = namespaces;
    }
    routes.Add(name, route);
    return route;
}

各參數如下:
routeName="Default", // 路由名稱
routeUrl= "{controller}/{action}/{id}", //帶有參數的 URL
defaults=new {controller = "Home", action = "Index", id = UrlParameter.Optional} //參數默認值
constraints=null
namespaces=null


這裡建立了一個Route實例並且把它加入到RouteCollection中了。
如果項目中有特殊的需要,只要我們註冊自己的IRouteHandler了,routes.Add(new Route("{controller}/{action}/{id}",new MvcRouteHandler())); 然後在裡面GetHttpHandler實現邏輯處理,就可以創建HttpHandler。

現在又回到 RouteData routeData = this.RouteCollection.GetRouteData(context);這句代碼中來,GetRouteData的程式如下:

public RouteData GetRouteData(HttpContextBase httpContext)
{
    using (this.GetReadLock())
    {
        foreach (RouteBase base2 in this)
        {
            RouteData routeData = base2.GetRouteData(httpContext);
            if (routeData != null)
            {
                return routeData;
            }
        }
    }

    return null;
}

這裡的base2就是先前調用MapRoute是添加的Route的。而Route的GetRouteData的方法如下:

public override RouteData GetRouteData(HttpContextBase httpContext)
{
    string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
    RouteValueDictionary values = this._parsedRoute.Match(virtualPath, this.Defaults);
    if (values == nullreturn null;

    RouteData data = new RouteData(this, this.RouteHandler);
    if (!this.ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest))
    return null;

    foreach (KeyValuePair<string, object> pair in values)
    {
        data.Values.Add(pair.Key, pair.Value);
    }

    if (this.DataTokens != null)
    {
        foreach (KeyValuePair<string, object> pair2 in this.DataTokens)
        {
            data.DataTokens[pair2.Key] = pair2.Value;
        }
    }

    return data;
}


以下是程式的解析,主要是幫助對於運作的程式結構上,有明確的了解。

主要下面這段,它是路由運作的監控:
RouteData data = new RouteData(this, this.RouteHandler);

下面這段程式是取得requestContext內容:
RequestContext requestContext = new RequestContext(context, routeData);
context.Request.RequestContext = requestContext;


在下面程式碼中它的主要作用獲取Httphandler這個監控:
IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);

對於MvcRouteHandler就是是表示如何取得一個Httphandler,最後直接返回了一個MvcHandler實例,如下:
protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) {
    requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext));
    return new MvcHandler(requestContext);
}

下面這段context.RemapHandler(httpHandler); 在HttpContext的RemapHandler方法中this._remapHandler = handler;,在HttpContext中有取得這些屬性,如下所示:

internal IHttpHandler RemapHandlerInstance
{
    get
    {
        return this._remapHandler;
    }
}

在HttpApplication的內部類別MaterializeHandlerExecutionStep中的HttpApplication.IExecutionStep.Execute()方法調用下面的程式碼:
if (httpContext.RemapHandlerInstance != null)
{
    httpContext.Handler = httpContext.RemapHandlerInstance;
}

看到MaterializeHandlerExecutionStep這個了類別名稱,在內部類別PipelineStepManager中BuildSteps方法有HttpApplication.IExecutionStep step = new HttpApplication.MaterializeHandlerExecutionStep(app);
app.AddEventMapping("ManagedPipelineHandler", RequestNotification.MapRequestHandler, false, step);

這樣對MVC整個路由應該有個大致的理解了。

-雲遊山水為知已逍遙一生而忘齡- 電腦神手