MVC 源码系列之路由(二)

MVCParseData和Match方法的实现

ParseData


那么首先要了解一下ParseData。

//namespace Route
public string Url
{
    get
    {
        return (this._url ?? string.Empty);
    }
    set
    {
        this._parsedRoute = RouteParser.Parse(value);
        this._url = value;
    }
}

其实在Route创建的时候 ,便将属性_parsedRoute赋值了。

public static ParsedRoute Parse(string routeUrl)
{
    if (routeUrl == null)
    {
        routeUrl = string.Empty;
    }
    if (IsInvalidRouteUrl(routeUrl))
    {
        throw new ArgumentException(SR.GetString("Route_InvalidRouteUrl"), "routeUrl");
    }
    IList<string> pathSegments = SplitUrlToPathSegmentStrings(routeUrl);
    Exception exception = ValidateUrlParts(pathSegments);
    if (exception != null)
    {
        throw exception;
    }
    return new ParsedRoute(SplitUrlToPathSegments(pathSegments));
}

 
internal static IList<string> SplitUrlToPathSegmentStrings(string url)
{
    List<string> list = new List<string>();
    if (!string.IsNullOrEmpty(url))
    {
        int index;
        for (int i = 0; i < url.Length; i = index + 1)
        {
            index = url.IndexOf('/', i);
            if (index == -1)
            {
                string str2 = url.Substring(i);
                if (str2.Length > 0)
                {
                    list.Add(str2);
                }
                return list;
            }
            string item = url.Substring(i, index - i);
            if (item.Length > 0)
            {
                list.Add(item);
            }
            list.Add("/");
        }
    }
    return list;
}

直接看代码有点困难,可以将参数代入这样会容易一点。url: "{controller}/{action}/{id}"。一开始判断/的位置。如果有的话,将第一个/之前的都截取下来,放到集合的一个位置,然后添加/,然后循环的时候位置加1 跳过/号,重新开始,到最后的时候将剩下的都添加到集合中。拿上面说的例子集合中的情况应该是这样的。['{controller}','/','{action}','/','{id}']。然后是判断路由的每个部分有没有不规范。最后就是构造ParsedRoute了,将IList转化为IList。每个string 我都称为段(文章后面我也会这样描述)。因为会出现复杂的段 比如{action}-{city}这种复杂的段。后面会说到怎么处理。

private static IList<PathSegment> SplitUrlToPathSegments(IList<string> urlParts)
{
    List<PathSegment> list = new List<PathSegment>();
    foreach (string str in urlParts)
    {
        if (IsSeparator(str))
        {
            list.Add(new SeparatorPathSegment());
        }
        else
        {
            Exception exception;
            IList<PathSubsegment> subsegments = ParseUrlSegment(str, out exception);
            list.Add(new ContentPathSegment(subsegments));
        }
    }
    return list;
}

internal static bool IsSeparator(string s)
{
    return string.Equals(s, "/", StringComparison.Ordinal);
}

如果是/的话 就生成一个SeparatorPathSegment放入集合中,不是/的话就在生成一个IList 。用/来分割每个段。复杂的段中会许多部分,c#
中用subsgment来表示子部分。字部分会有两种形式,变量(参数部分)和常量(文字部分)。变量用ParameterSubsegment(参数部分)表示,常量LiteralSubsegment(文字部分)表示。看看里面构造,是如何将路由模板分解为一个一个片段的。

private static IList<PathSubsegment> ParseUrlSegment(string segment, out Exception exception)
{
    int startIndex = 0;
    List<PathSubsegment> list = new List<PathSubsegment>();
    while (startIndex < segment.Length)
    {
        int num2 = IndexOfFirstOpenParameter(segment, startIndex);
        if (num2 == -1)
        {
            string str3 = GetLiteral(segment.Substring(startIndex));
            if (str3 == null)
            {
                object[] args = new object[] { segment };
                exception = new ArgumentException(string.Format(CultureInfo.CurrentUICulture, "Route_MismatchedParameter", args), "routeUrl");
                return null;
            }
            if (str3.Length > 0)
            {
                list.Add(new LiteralSubsegment(str3));
            }
            break;
        }
        int index = segment.IndexOf('}', num2 + 1);
        if (index == -1)
        {
            object[] objArray2 = new object[] { segment };
            exception = new ArgumentException(string.Format(CultureInfo.CurrentUICulture,"Route_MismatchedParameter", objArray2), "routeUrl");
            return null;
        }
        string literal = GetLiteral(segment.Substring(startIndex, num2 - startIndex));
        if (literal == null)
        {
            object[] objArray3 = new object[] { segment };
            exception = new ArgumentException(string.Format(CultureInfo.CurrentUICulture, "Route_MismatchedParameter", objArray3), "routeUrl");
            return null;
        }
        if (literal.Length > 0)
        {
            list.Add(new LiteralSubsegment(literal));
        }
        string parameterName = segment.Substring(num2 + 1, (index - num2) - 1);
        list.Add(new ParameterSubsegment(parameterName));
        startIndex = index + 1;
    }
    exception = null;
    return list;
}
//获得变量的第一个{的位置
private static int IndexOfFirstOpenParameter(string segment, int startIndex)
{
    while (true)
    {
        startIndex = segment.IndexOf('{', startIndex);
        if (startIndex == -1)
        {
            return -1;
        }
        if (((startIndex + 1) == segment.Length) || (((startIndex + 1) < segment.Length) && (segment[startIndex + 1] != '{')))
        {
            return startIndex;
        }
        startIndex += 2;
    }
}
//获得变量中的文字
private static string GetLiteral(string segmentLiteral)
{
    string str = segmentLiteral.Replace("{{", "").Replace("}}", "");
    if (!str.Contains("{") && !str.Contains("}"))
    {
        return segmentLiteral.Replace("{{", "{").Replace("}}", "}");
    }
    return null;
}

可以看到,将段传入ParseUrlSegment方法。首先创建了一个startIndex,这个是每一次开始的位置,一个段可能是这样的 {action}-{city},里面就包含了两个元素,所以每次开始处理完前一个部分的时候会用startIndex变量记录第二次开始的位置。开始通过IndexOfFirstOpenParameter获得了一个num2,如果返回-1也就是找不到{号,那就直接当文字进行处理。方法内部实现其实挺简单的,获得{的初始位置,如果找不到的话会放回-1。后面还有个判断,条件会比较复杂

((startIndex + 1) == segment.Length) || (((startIndex + 1) < segment.Length) && (segment[startIndex + 1] != '{'))

这个条件是说,只有在{的位置不在最后并且是{{的时候才StartIndex+=2跳过{{寻找接下去的{位置。条件看不懂也没事,这个也就是寻找段中的有效的第一个{位置。然后返回出去,会做判断,如果为位置-1直接通过GetLiteral(获得文字)的方法去过滤文字,如果里面含有{{就被换成{符号。其实没看懂这两个方法,只看懂了是这样去执行的,不知道为什么要这么去执行。如果不是-1,获取}的位置,然后看看{的之前有没有符号,就是{action}-{city}中的这个-,当然在第一次执行的时候{action}前面没有任何符号,当执行到{city}的时候

string literal = GetLiteral(segment.Substring(startIndex, num2 - startIndex));

就会通过这个区获取 - 号,也就是默认的文本。添加到List中。最后在{的位置和}的位置之间,将文字表示的变量放到ParameterSubsegment。然后index+1进行下一轮的筛选。应为前面两个不懂的方法,会造成一些奇怪的现象,比如{controller}/{{city}}/{id} 这样的路由的话,中的{{city}}将会变成一个文本,如果改成三个{}包涵的话,就可以将{号转义成功了。有兴趣的同学可以试试。

Match的实现

可以看出,集合中无非就是SeparatorPathSegment和ContentPathSegment中又包含很多个subsegment。最后就是这个Match的方法了。

public RouteValueDictionary Match(string virtualPath, RouteValueDictionary defaultValues)
{
    IList<string> source = RouteParser.SplitUrlToPathSegmentStrings(virtualPath);
    if (defaultValues == null)
    {
        defaultValues = new RouteValueDictionary();
    }
    RouteValueDictionary matchedValues = new RouteValueDictionary();
    bool flag = false;
    bool flag2 = false;
    for (int i = 0; i < this.PathSegments.Count; i++)
    {
        PathSegment segment = this.PathSegments[i];
        if (source.Count <= i)
        {
            flag = true;
        }
        string a = flag ? null : source[i];
        if (segment is SeparatorPathSegment)
        {
            if (!flag && !string.Equals(a, "/", StringComparison.Ordinal))
            {
                return null;
            }
        }
        else
        {
            ContentPathSegment contentPathSegment = segment as ContentPathSegment;
            if (contentPathSegment != null)
            {
                if (contentPathSegment.IsCatchAll)
                {
                    this.MatchCatchAll(contentPathSegment, source.Skip<string>(i), defaultValues, matchedValues);
                    flag2 = true;
                }
                else if (!this.MatchContentPathSegment(contentPathSegment, a, defaultValues, matchedValues))
                {
                    return null;
                }
            }
        }
    }
    if (!flag2 && (this.PathSegments.Count < source.Count))
    {
        for (int j = this.PathSegments.Count; j < source.Count; j++)
        {
            if (!RouteParser.IsSeparator(source[j]))
            {
                return null;
            }
        }
    }
    if (defaultValues != null)
    {
        foreach (KeyValuePair<string, object> pair in defaultValues)
        {
            if (!matchedValues.ContainsKey(pair.Key))
            {
                matchedValues.Add(pair.Key, pair.Value);
            }
        }
    }
    return matchedValues;
}

将之前的获得的virtualPath传入方法,通过SplitUrlToPathSegmentStrings分割,前面说过这个是通过/进行分组的。然后循环PathSegments。在上面说过了,这个是将模板转化成ParseData,里面包含了两种类型SeparatorPathSegment和ContentPathSegment。会通过每一个部分去一一匹配。如果没有匹配上就返回null。判断实际传入的url经过/分割之后的数量是不是比模板的数量少。如果url的长度少于模板长度,将flag为true。如果是/号也就是SeparatorPathSegment就跳过,如果是ContentPathSegment有内容的话,this.MatchContentPathSegment通过这个方法进行判断如果没有符合的话就返回null,并且这个方法会将模板中变量于url中对应的部分放到字典中去。如果url的长度大于模板的长度,而且大于的长度部分不是/号,便视为不匹配。最后的时候,使用defaultValues,url中没有传入模板的值得话将会使用默认值去代替。理一下最后对比的逻辑。

  1. 获取url的分割后的组
  2. 进行循环比对
    • SeparatorPathSegment
    • ContentPathSegment MatchContentPathSegment方法将模板中的值和url中进行匹配放入到字典中。
  3. 测试url的时间长度是不是比模版的长
  4. 将默认值放入的最后的集合中去

现在关键是的是 MatchContentPathSegment方法的实现。

private bool MatchContentPathSegment(ContentPathSegment routeSegment, string requestPathSegment, RouteValueDictionary defaultValues, RouteValueDictionary matchedValues)
{

    if (string.IsNullOrEmpty(requestPathSegment))
    {
        if (routeSegment.Subsegments.Count <= 1)
        {
            object obj2;
            ParameterSubsegment subsegment3 = routeSegment.Subsegments[0] as ParameterSubsegment;
            if (subsegment3 == null)
            {
                return false;
            }
            if (defaultValues.TryGetValue(subsegment3.ParameterName, out obj2))
            {
                matchedValues.Add(subsegment3.ParameterName, obj2);
                return true;
            }
        }
        return false;
    }
    
    int LastIndex = requestPathSegment.Length;
    int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1;
    ParameterSubsegment subsegment = null;
    LiteralSubsegment subsegment2 = null;
    
    while (indexOfLastSegmentUsed >= 0)
    {
        //第一部分
        int newLastIndex = LastIndex;
        ParameterSubsegment subsegment4 = routeSegment.Subsegments[indexOfLastSegmentUsed] as ParameterSubsegment;
        if (subsegment4 != null)
        {
            subsegment = subsegment4;
        }
        else
        {
            LiteralSubsegment subsegment5 = routeSegment.Subsegments[indexOfLastSegmentUsed] as LiteralSubsegment;
            if (subsegment5 != null)
            {
                subsegment2 = subsegment5;
                int startIndex = LastIndex - 1;
                if (subsegment != null)
                {
                    startIndex--;
                }
                if (startIndex < 0)
                {
                    return false;
                }
                int indexOfLiteral = requestPathSegment.LastIndexOf(subsegment5.Literal, startIndex, StringComparison.OrdinalIgnoreCase);
                if (indexOfLiteral == -1)
                {
                    return false;
                }
                if ((indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1)) && ((indexOfLiteral + subsegment5.Literal.Length) != requestPathSegment.Length))
                {
                    return false;
                }
                newLastIndex = indexOfLiteral;
            }
        }
        //第二部分
        if ((subsegment != null) && (((subsegment2 != null) && (subsegment4 == null)) || (indexOfLastSegmentUsed == 0)))
        {
            int parameterStartIndex;
            int parameterTextLength;
            if (subsegment2 == null)
            {   
                //没有文字部分 全都是参数部分
                if (indexOfLastSegmentUsed == 0)
                {
                    parameterStartIndex = 0;
                }
                else
                {
                    parameterStartIndex = newLastIndex;
                }
                parameterTextLength = LastIndex;
            }
            else if ((indexOfLastSegmentUsed == 0) && (subsegment4 != null))
            {
                //文字部分在参数部分后面
                parameterStartIndex = 0;
                parameterTextLength = LastIndex;
            }
            else
            {
                //文字部分在参数前面
                parameterStartIndex = newLastIndex + subsegment2.Literal.Length;
                parameterTextLength = LastIndex - parameterStartIndex;
            }
            string str = requestPathSegment.Substring(parameterStartIndex, parameterTextLength);
            if (string.IsNullOrEmpty(str))
            {
                return false;
            }
            matchedValues.Add(subsegment.ParameterName, str);
            subsegment = null;
            subsegment2 = null;
        }
        LastIndex = newLastIndex;
        indexOfLastSegmentUsed--;
    }
    
    //第三部分
    if (LastIndex != 0)
    {
        return (routeSegment.Subsegments[0] is ParameterSubsegment);
    }
    return true;
}

代码有点长,主要分为两部分。requestPathSegment为空和不为空的两种情况。

如果requestPathSegment为空的话,获取ParameterSubsegment,然后从传入的默认值字典中去取值。如果在默认值中有值的话添加到matchedValues中,如果没有返回false(也就是没有匹配成功)。

requestPathSegment有值的话,先获取url的长度和ContentPathSegment里面的Subsegments个数(前面时候过,每个段可能会有多个部分)。分别创建了ParameterSubsegment和LiteralSubsegment来判断这个部分是参数还是文字部分。将subsegments转化为ParameterSubsegment参数类型如果不为空的话便赋给subsegment,或者是LiteralSubsegment文字类型。如果是LiteralSubsegment,将subsegment2 = subsegment5赋给subsegment2。int startIndex = LastIndex(==requestPathSegment.Length) - 1,如果这个段中 只有文字或者是参数部分是在文字部分前面(例如:{actiuon}page)这样的就不用--,如果是其他的情况需要startIndex--。如果小于0了之后便返回false。
这个变量的作用就是下面这句的参数,找到文字的位置。

int indexOfLiteral = requestPathSegment.LastIndexOf(subsegment5.Literal, startIndex, StringComparison.OrdinalIgnoreCase);

找到模板中的文字在url中的位置,开始位置就是startIndex。如果找不到返回false,匹配失败。然后又有一个条件判断

 if ((indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1)) && ((indexOfLiteral + subsegment5.Literal.Length) != requestPathSegment.Length))

如果indexOfLastSegmentUsed是最后一个subsegment并且是LiteralSubsegment,获得的文字的长度和请求的长不同的话,也放回false。indexOfLiteral这个变量就是说明如果在既有参数又有文字的段里面的话文字的位置。这个判断就判断了文字部分是否相同(首先看是否含有模板中的字符,然后判断长度是否相同)。如果这个判断为false的话就newLastIndex = indexOfLiteral。接下去又是一个长判断。

 if ((subsegment != null) && (((subsegment2 != null) && (subsegment4 == null)) || (indexOfLastSegmentUsed == 0)))

上面的情况大致分了几种情况来讨论着个条件。里面的具体实现是什么呢?开始创建了parameterStartIndex和parameterTextLength也就是参数的开始的位置和长度,最后给在requestPathSegment中截取出参数的值,和subsegment的name属性放到字典中去。里面还是分情况对parameterStartIndex和indexOfLastSegmentUsed进行赋值。

  1. {action} 的情况,只剩下一个参数没有匹配, parameterStartIndex = 0; parameterTextLength = LastIndex;
  2. {aciont}page 的情况就是既有参数也有文字 subsegment2不为null 如果参数部分已经是最后了,也就是在参数部分在文字部分的前面时,parameterStartIndex = 0; parameterTextLength = LastIndex;
  3. 如果是 Page{action} 就会走第三个判断 parameterStartIndex = newLastIndex + subsegment2.Literal.Length; parameterTextLength = LastIndex - parameterStartIndex;

其实判断里面分了三部分:

  • 第一部分获取参数部分或者是文字部分的元素保存在subsegment和subsegment2里面(随便说一下subsegment4和subsegment5是表明当前循环中的Subsegment是文字部分还是参数部分)。 如果是文字部分的话找出文字部分在url中的位置,如果这个段只有文字且长度不对,则返回false
  • 第二部分,首先subsegment要有参数部分(有参数部分才能发到字典里面去) 并且 如果有段中由文字部分的话需要循环到文字部分才会进到循环里面。
    • 没有文字部分,都是参数部分,如果只有一个那就直接将parameterStartIndex为0,parameterStartIndex=LastIndex(=requestPathSegment.Length)也就是所有的。如果有两个subsegment会将所有的数据都给第一个sub(例如:{action}{id} url:RouteTest 字典中的情况为[action,RouteTest],[id,""])
    • 有文字部分在参数部分后面,但是参数部分在文字部分的前面({acion}page 而且参数部分前面就没有文字部分的情况),这种情况会先处理文字部分,再处理文字部分的时候会将newLastIndex = indexOfLiteral文字开头的部分给newLastIndex变量,然后变量间接的parameterTextLength这个长度。
    • 有文字部分在参数部分前面(例如:page{action}),会通过newLastIndex + subsegment2.Literal.Length 最后的位置加文字的长度,parameterTextLength = LastIndex - parameterStartIndex;来算出参数值得位置。
    • 其实所有的情况都可以分解为以上三种情况 举个例子:{action}page{action} 系统就会分解成 page{action}和{action}去处理,这就是上面的第三种情况和第一中情况。
  • 第三部分 最后的LastIndex会为0 ,如果最后还没有为0的情况,判断一下参数的第一个是不是ParameterSubsegment
原文地址:https://www.cnblogs.com/shaoqi/p/7359934.html