首页 > 代码库 > Asp.Net MVC-02-路由

Asp.Net MVC-02-路由

前面关于MVC之前的篇章中我们讲到UrlRoutingModule在PostResolveRequestCache事件中通过查找路由决定使用哪个HttpHandler来处理请求,那路由的查找匹配到底是如何实现的呢。

基本

来看在UrlRoutingModule中获取路由数据的方法:

RouteData routeData = http://www.mamicode.com/RouteCollection.GetRouteData(context);

来看RouteCollection中GetRouteData的实现:

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

显然,该方法会对路由表中注册的所有路由进行匹配解析,匹配解析成功即返回,匹配解析的方法是GetRouteData,该方法实现在Route类中(重写其基类RouteBase的方法)

    public override RouteData GetRouteData(HttpContextBase httpContext) {
        string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
        RouteValueDictionary values = _parsedRoute.Match(requestPath, Defaults);
        if (values == null) {
            return null;
        }
        RouteData routeData = new RouteData(this, RouteHandler);
        if (!ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest)) {
            return null;
        }
        foreach (var value in values) {
            routeData.Values.Add(value.Key, value.Value);
        }
        if (DataTokens != null) {
            foreach (var prop in DataTokens) {
                routeData.DataTokens[prop.Key] = prop.Value;
            }
        }
        return routeData;
    }

基本的处理过程是先通过ParsedRoute对象的Mactch方法获取参数(从url或默认值中),然后通过ProcessConstraints处理约束条件。

先来看ParsedRoute的Mactch方法:

    public RouteValueDictionary Match(string virtualPath, RouteValueDictionary defaultValues) {
        ……
        for (int i = 0; i < PathSegments.Count; i++) {
            PathSegment pathSegment = PathSegments[i];
            if (requestPathSegments.Count <= i) {
                ranOutOfStuffToParse = true;
            }
            string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i];
            if (pathSegment is SeparatorPathSegment) {
                ……
            }
            else {
                ContentPathSegment contentPathSegment = pathSegment as ContentPathSegment;
                if (contentPathSegment != null) {
                    if (contentPathSegment.IsCatchAll) {
                        ……
                        MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues);
                        usedCatchAllParameter = true;
                    }
                    else {
                        if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues)) {
                            return null;
                        }
                    }
                }
                ……
            }
        }
        if (!usedCatchAllParameter) {
            if (PathSegments.Count < requestPathSegments.Count) {
                for (int i = PathSegments.Count; i < requestPathSegments.Count; i++) {
                    if (!RouteParser.IsSeparator(requestPathSegments[i])) {
                        return null;
                    }
                }
            }
        }
        if (defaultValues != null) {
            foreach (var defaultValue in defaultValues) {
                if (!matchedValues.ContainsKey(defaultValue.Key)) {
                    matchedValues.Add(defaultValue.Key, defaultValue.Value);
                }
            }
        }
        return matchedValues;
    }

这里使用了一个重要的参数IList<PathSegment> PathSegments,这是通过对注册路由的url进行拆分(以“/”为分隔符)获取了各种分段信息(比如分隔字符、参数名等等),然后通过SplitUrlToPathSegments方法创建PathSegments。

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

创建的PathSegment分为两种:SeparatorPathSegment和ContentPathSegment,SeparatorPathSegment是分隔字符,ContentPathSegment用于参数匹配。获取了路由的分段信息,然后将当前请求的也按照类似方式分段以后就可以进行匹配和获取参数了。

参数获取

查看ParsedRoute对象的Mactch方法,关于匹配和获取参数的关键之处有两处,一个是普通参数的匹配方法MatchContentPathSegment,一个是Catch-All参数的匹配方法MatchCatchAll。

首先来看MatchContentPathSegment:

    private bool MatchContentPathSegment(ContentPathSegment routeSegment, string requestPathSegment, RouteValueDictionary defaultValues, RouteValueDictionary matchedValues) {
            if (String.IsNullOrEmpty(requestPathSegment)) {
                if (routeSegment.Subsegments.Count > 1) {
                    return false;
                }
                ParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as ParameterSubsegment;
                if (parameterSubsegment == null) {
                    return false;
                }
                object parameterValue;
                if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue)) {
                    matchedValues.Add(parameterSubsegment.ParameterName, parameterValue);
                    return true;
                }
                else {
                    return false;
                }
            }
            int lastIndex = requestPathSegment.Length;
            int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1;
            ParameterSubsegment parameterNeedsValue = null;
            LiteralSubsegment lastLiteral = null;
            while (indexOfLastSegmentUsed >= 0) {
                int newLastIndex = lastIndex;
                ParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as ParameterSubsegment;
                if (parameterSubsegment != null) {
                    parameterNeedsValue = parameterSubsegment;
                }
                else {
                    LiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as LiteralSubsegment;
                    if (literalSubsegment != null) {
                        lastLiteral = literalSubsegment;
                        int startIndex = lastIndex - 1;
                        if (parameterNeedsValue != null) {
                            startIndex--;
                        }
                        if (startIndex < 0) {
                            return false;
                        }
                        int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase);
                        if (indexOfLiteral == -1) {
                            return false;
                        }
                        if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1)) {
                            if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length) {
                              return false;
                            }
                        }
                        newLastIndex = indexOfLiteral;
                    }
                    else {
                        Debug.Fail("Invalid path segment type");
                    }
                }
                if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0))) {
                    int parameterStartIndex;
                    int parameterTextLength;
                    if (lastLiteral == null) {
                        if (indexOfLastSegmentUsed == 0) {
                            parameterStartIndex = 0;
                        }
                        else {
                            parameterStartIndex = newLastIndex;
                            Debug.Fail("indexOfLastSegementUsed should always be 0 from the check above");
                        }
                        parameterTextLength = lastIndex;
                    }
                    else {
                        if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null)) {
                            parameterStartIndex = 0; 
                            parameterTextLength = lastIndex;
                        }
                        else {
                            parameterStartIndex = newLastIndex + lastLiteral.Literal.Length;
                            parameterTextLength = lastIndex - parameterStartIndex;
                        }
                    }
                    string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength);
                    if (String.IsNullOrEmpty(parameterValueString)) {
                        return false;
                    }
                    else {                        
matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString); } parameterNeedsValue
= null; lastLiteral = null; } lastIndex = newLastIndex; indexOfLastSegmentUsed--; } return (lastIndex == 0) || (routeSegment.Subsegments[0] is ParameterSubsegment); }

 

这里的代码比较复杂,相关的代码也比较多,这里只做一个大体的说明:首先对于路由中有的分段而请求url中没有的不允许有多个参数并且必须已设置默认参数(默认参数下一节讲);其次对在段中的多个参数段和参数之间的分隔符(路由url中指定)进行匹配,实际要达到的目的是从左至右的贪婪匹配。

至于MatchCatchAll相对来说比较简单。

    private void MatchCatchAll(ContentPathSegment contentPathSegment, IEnumerable<string> remainingRequestSegments, RouteValueDictionary defaultValues, RouteValueDictionary matchedValues) {
            string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray());
            ParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as ParameterSubsegment;
            object catchAllValue;
            if (remainingRequest.Length > 0) {
                catchAllValue = remainingRequest;
            }
            else {
                defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue);
            }
            matchedValues.Add(catchAllSegment.ParameterName, catchAllValue);
        }

需要注意的是MatchCatchAll是最后一个进行匹配的参数,会将请求url中不含请求字符串的剩余部分全部匹配,即使参数匹配的结果为空(包括默认参数)仍然会匹配成功。

默认值

默认值是一个字典对象,上面已经看到每次匹配参数时如果参数为空则会试图去默认值中获取参数值。除此以外查看ParsedRoute的Mactch方法,在最后一段

    if (defaultValues != null) {
        foreach (var defaultValue in defaultValues) {
            if (!matchedValues.ContainsKey(defaultValue.Key)) {
                matchedValues.Add(defaultValue.Key, defaultValue.Value);
            }
        }
    }

可以得知在默认参数中即使注册的路由中没有设定参数也会把默认参数传到RouteData中。

约束

完成匹配和参数获取之后就进入到检查约束条件(ProcessConstraints)了,在Route中实现。

    private bool ProcessConstraints(HttpContextBase httpContext, RouteValueDictionary values, RouteDirection routeDirection) {
            if (Constraints != null) {
                foreach (var constraintsItem in Constraints) {
                    if (!ProcessConstraint(httpContext, constraintsItem.Value, constraintsItem.Key, values, routeDirection)) {
                        return false;
                    }
                }
            }
            return true;
        }

    protected virtual bool ProcessConstraint(HttpContextBase httpContext, object constraint, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
            IRouteConstraint customConstraint = constraint as IRouteConstraint;
            if (customConstraint != null) {
                return customConstraint.Match(httpContext, this, parameterName, values, routeDirection);
            }
            string constraintsRule = constraint as string;
            if (constraintsRule == null) {
                throw new InvalidOperationException(String.Format(
                    CultureInfo.CurrentUICulture,
                    SR.GetString(SR.Route_ValidationMustBeStringOrCustomConstraint),
                    parameterName,
                    Url));
            }
            object parameterValue;
            values.TryGetValue(parameterName, out parameterValue);
            string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture);
            string constraintsRegEx = "^(" + constraintsRule + ")$";
            return Regex.IsMatch(parameterValueString, constraintsRegEx,
                RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
        }

可以看到有两种方式的约束条件,一种是实现IRouteConstraint接口,甚至可以自定义的约束,另外一种就是通过正则表达式进行验证。

前面讲到在PostResolveRequestCache事件中通过路由匹配之后会创建HttpHandler来处理请求,而且已经在当前的请求中设置了路由的参数值(包括Controller、action等信息),下一篇就来看MvcHandler是如何根据这些信息创建Controller乃至调用action的。

Asp.Net MVC-02-路由