使用asp.net mvc以来,UrlRouting的处理就是一个非常关键的问题,由于使用的不小心,经常导致我们无法得到预期的结果,这的确是个很麻烦的问题,于是很多朋友推测是MVC框架的bug,到底事实如何呢?今天我便尽力探索系统中UrlRouting到底是如何工作的,希望能找出问题的关键.
总所周知,Asp.Net MVC框架一般使用Global.asax在程序第一次启动的时候初始化RouteCollection,Preview3,我们一般使用RouteCollection. MapRoute方法来添加新的规则.然后,系统理论上会非常听话执行我们给出的规则,然后我们直接或者间接在页面中使用UrlHelper提供的方法处理Url,UrlHelper使用路由而非路径的方式定义url,能给我们更大的方便,但是问题来了,很多朋友发现UrlHelper并不是那么听话的,可以说有时候会给出一个莫名其妙的地址.为了解开这个问题,我们得先看看系统到底怎么来处理这些规则的.
首先我们把这个MapRoute方法找出来,查询源代码:

public static void MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints){
           
if (routes == null) {
               
throw new ArgumentNullException("routes");
            }

           
if (url == null) {
               
throw new ArgumentNullException("url");
            }


            Route route
= new Route(url, new MvcRouteHandler()) {
                Defaults
= new RouteValueDictionary(defaults),
                Constraints
= new RouteValueDictionary(constraints)
            }
;

           
if (String.IsNullOrEmpty(name)) {
               
// Add unnamed route if no name given
                routes.Add(route);
            }

           
else {
                routes.Add(name, route);
            }

        }



  这是一个扩展方法,我们看到这实际上是简化了PreView2中添加路由的方式.这儿仍然和以前一样使用routes.Add方法来添加路由,由于System.Web.Routing没有开发源码,只好使用反编译该程序集来研究,我们再看RouteCollection的关键定义:

public
class RouteCollection : Collection<RouteBase>
{
   
// Fields
private Dictionary<string, RouteBase> _namedMap;
xxx…



  这表明实际上RouteCollection维护了两个容器,一个是Collection<RouteBase>,一个是Dictionary<string, RouteBase> _namedMap,再查看Add方法的代码:

public void Add(string name, RouteBase item)
{
   
if (item == null)
   
{
       
throw new ArgumentNullException("item");
    }

   
if (!string.IsNullOrEmpty(name) && this._namedMap.ContainsKey(name))
   
{
       
throw new ArgumentException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.RouteCollection_DuplicateName, new object[] { name }), "name");
    }

   
base.Add(item);
   
if (!string.IsNullOrEmpty(name))
   
{
       
this._namedMap.set_Item(name, item);
    }

}



  如果未提供name参数,则直接使用Collection提供的Add方法添加Route,这时并没有向_ namedMap添加route,只有提供了name,且提供的name满足!IsNullOrEmpty参数才会向_namedMap添加规则.ok,这下明白了RouteCollection是如何存储路由规则了,我们继续看UrlHelper部分和Url有关的主要提供了Action, ContentRouteUrl3个方法,RouteUrlAction方法则都是调用了UrlHelper.GenerateUrl方法,至于其他和Url有关的部分,HtmlHelper也都是直接或者间接调用UrlHelper.GenerateUrl方法.我们一个个查看.
首先看Action,该方法会给出一个连接到所提供的actionurl,有好几个重载,但是总结起来都是调用return GenerateUrl(null /* routeName */, actionName,xxx,xxx)的模式,也就是说前面所有Action间接调用GenerateUrl时候前两个参数固定,一个是null,一个是actionName,而在RouteUrl中则不同,会根据不同的重载模式来,既有需要routeName,也有不需要routeName.现在关键就是GenerateUrl方法了,该方法代码如下:

private string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary valuesDictionary) {
           
return GenerateUrl(routeName, actionName, controllerName, valuesDictionary, RouteCollection, ViewContext);
        }


       
internal static string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary valuesDictionary, RouteCollection routeCollection, ViewContext viewContext) {
           
if (actionName != null) {
               
if (valuesDictionary.ContainsKey("action")) {
                   
throw new ArgumentException(
                        String.Format(
                            CultureInfo.CurrentUICulture,
                            MvcResources.Helper_DictionaryAlreadyContainsKey,
                           
"action"),
                       
"actionName");
                }

                valuesDictionary.Add(
"action", actionName);
            }

           
if (controllerName != null) {
               
if (valuesDictionary.ContainsKey("controller")) {
                   
throw new ArgumentException(
                        String.Format(
                            CultureInfo.CurrentUICulture,
                            MvcResources.Helper_DictionaryAlreadyContainsKey,
                           
"controller"),
                       
"controllerName");
                }

                valuesDictionary.Add(
"controller", controllerName);
            }

VirtualPathData vpd;
           
if (routeName != null) {
                vpd
= routeCollection.GetVirtualPath(viewContext, routeName, valuesDictionary);
            }

           
else {
                vpd
= routeCollection.GetVirtualPath(viewContext, valuesDictionary);
            }


           
if (vpd != null) {
               
return vpd.VirtualPath;
            }

           
return null;
        }



 

关键是第二个,分析下这个方法,它要求必须唯一提供action,且不能重复提供controller,然后,对于路径的查找,如果提供了routeName,则系统会使用GetVirtualPath(viewContext, routeName, valuesDictionary);否则使用routeCollection.GetVirtualPath(viewContext, valuesDictionary);


我们再看这两个方法,照样反编译下,具体代码如下:


public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
    requestContext
= this.GetRequestContext(requestContext);
   
using (this.GetReadLock())
   
{
       
using (IEnumerator<RouteBase> enumerator = base.GetEnumerator())
       
{
           
while (enumerator.MoveNext())
           
{
                VirtualPathData virtualPath
= enumerator.get_Current().GetVirtualPath(requestContext, values);
               
if (virtualPath != null)
               
{
                    virtualPath.VirtualPath
= GetUrlWithApplicationPath(requestContext, virtualPath.VirtualPath);
                   
return virtualPath;
                }

            }

        }

    }

   
return null;
}


public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values)
{
    RouteBase base2;
   
bool flag;
    requestContext
= this.GetRequestContext(requestContext);
   
if (string.IsNullOrEmpty(name))
   
{
       
return this.GetVirtualPath(requestContext, values);
    }

   
using (this.GetReadLock())
   
{
        flag
= this._namedMap.TryGetValue(name, ref base2);
    }

   
if (!flag)
   
{
       
throw new ArgumentException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.RouteCollection_NameNotFound, new object[] { name }), "name");
    }

    VirtualPathData virtualPath
= base2.GetVirtualPath(requestContext, values);
   
if (virtualPath == null)
   
{
       
return null;
    }

    virtualPath.VirtualPath
= GetUrlWithApplicationPath(requestContext, virtualPath.VirtualPath);
   
return virtualPath;
}



 

第一个方法,由于没有提供name,于是遍历自身容器查询,第二个方法,如果提供的name不为空,则直接使用_namedMap获取,这是一个字典结构,如果没找到,则抛出异常.到这儿我们可以发现一个问题了,在没有提供routeName的情况下是遍历查询,只要找到满足条件的就返回了,那么如果有多可匹配的情况会如何呢?由于算法的特性,必然会返回第一个找到的结果,到这儿便焕然大悟了.如果我们的route初始化这样写:


routes.MapRoute("default", "default.aspx", new { controller = "demo", action = "test" });


routes.MapRoute("testroute ", "demo/{action}", new { controller = "demo", action = "test" });


那么我们用Url.Action(“demo”)的时候永远返回的是”default.aspx”,而不是可能需要的”demo/test”,于是在书写route规则的时候,必须做到从一般到特殊的规则,让系统从一般规则开始找,找不到再找特殊规则.当然,通过Url.RouteUrl便没有问题啦.因此,上面更规则的写法是:


routes.MapRoute("testroute", "demo/{action}", new { controller = "demo", action = "test" });


routes.MapRoute("default", "default.aspx", new { controller = "demo", action = "test" });


而对以这种定义:


Url.Action(“test”)Url. RouteUrl(“testroute”)返回值将是一样的,都是”demo/test”.如果有参数,比如:


routes.MapRoute("testroute", "demo/{action}/{id}", new { controller = "demo", action = "test", id="1" });


routes.MapRoute("default", "default.aspx", new { controller = "demo", action = "test", id="0" });


则下面两种调用是等价的:


<%=Url.Action("test", new { id = "2" }) %>


<%=Url.RouteUrl("testroute", new { id = "2" })%>


都会输出:” demo/test/2”;同时由于这时不适用routename的查找也同时根据了actionid,因此上面的规则顺序改变下也不会出问题.


但是如果调用:


<%=Url.Action("test") %>


<%=Url.RouteUrl("testroute")%>


这时分别返回的是:


/default.aspx
/demo


这时想想上面的代码,自然可以理解原因了.