Asp.Net MVC-02-路由

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

基本

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

RouteData routeData = 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的。

时间: 2024-10-21 07:07:43

Asp.Net MVC-02-路由的相关文章

[学习笔记] 理解ASP.NET MVC的路由系统

引言 路由,正如其名,是决定消息经由何处被传递到何处的过程.也正如网络设备路由器Router一样,ASP.NET MVC框架处理请求URL的方式,同样依赖于一张预定义的路由表.以该路由表为转发依据,请求URL最终被传递给特定Controller的特定Action进行处理.而在相反的方向上,MVC框架的渲染器同样要利用这张路由表,生成最终的HTML页面并返回URL.所以,理解整个ASP.NET MVC的路由系统,有两个必须出现的关键元素:Controller与Action,有两个方向的操作:传入的

asp.net MVC 5 路由 Routing

ASP.NET MVC ,一个适用于WEB应用程序的经典模型 model-view-controller 模式.相对于web forms一个单一的整块,asp.net mvc是由连接在一起的各种代码层所组成. 最近又接触了关于asp.net mvc的项目,又重拾以前的记忆,感觉忘了好多,特此记录. 首先,来说说路由Routing. ASP.NET MVC 不再是要依赖于物理页面了,你可以使用自己的语法自定义URL,通过这些语法来指定资源和操作.语法通过URL模式集合表达,也称为路由. 路由是代表

asp.net mvc 伪静态路由配置

asp.net mvc实现伪静态路由必须按如下方式设置好,才能访问 .htm 或者.html页面 C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll 来自为知笔记(Wiz)

ASP.NET MVC API 路由生成规则

我们都知道调用ASP.NET MVC的某些API函数(诸如:Url.Action.RedirectToAction等)可以生成URL,ASP.NET MVC会根据调用API函数时传入的参数去匹配系统定义的路由(Route),然后通过匹配成功的路由去生成相应的URL. ASP.NET MVC会依次根据如下三个规则生成URL: 调用ASP.NET MVC API函数时传入的参数信息 当前请求的URL(就是Request.Url)和系统中定义路由匹配(按照路由表定义的顺序,从上往下匹配)后的匹配值 系

Asp.Net MVC的路由

通过前面几篇博文的介绍,现在我们已经清楚了asp.net请求管道是怎么一回事了,这篇博文来聊聊MVC的路由. 其实MVC的请求管道和Asp.Net请求管道一样,只是MVC扩展了UrlRoutingModule的动作.我们知道MVC网站启动后第一个请求会执行Global.asax文件中的Application_Start方法,完成一些初始化工作,其中就会注册路由,先来看下面一张图,该图可以直观的展示了MVC执行的流程. 结合上图,我们一起来看看代码是如何实现路由的注册的. protected vo

ASP.NET MVC——URL路由

在MVC之前,ASP.NET假设请求的URL与服务器上的文件之间有关联,服务器接受请求,并输出相应的文件.而在引入MVC后,请求是由控制器的动作方法来处理的.为了处理URL,便引入了路由系统. 首先我们来创建一个基础项目用来演示.代码如下: 1 public class HomeController : Controller 2 { 3 public ActionResult Index() 4 { 5 ViewBag.Controller = "Home"; 6 ViewBag.Ac

ASP.NET MVC之路由深究

MVC的强大之处之一当然是路由,这是几年前一位牛人给我说过的话,本人深感认同.今天就再次探究. 首先新建一个空的MVC项目,我们会发现在RouteConfig类中存在一个默认的路由配置,通常我会在这里的路由中添加一个命名空间,以防止路由配置冲突 routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home&quo

asp.net MVC动态路由

项目中遇到需要动态生成控制器和视图的. 于是就折腾半天,动态生成控制器文件和视图文件,但是动态生成控制器不编译是没法访问的. 找人研究后,得到要领: 1.放在App_Code文件夹内 2.不要命名空间 功能虽然实现了,可是觉得这个发放实在有些挫,心里老挂念这个事情.想着既然使用MVC,能不能实现动态路由访问呢? 果然找到两篇相关的文章,解决了问题: 1.http://www.cnblogs.com/gyche/p/5216361.html 2.http://stackoverflow.com/q

ASP.NET MVC自定义路由 - 实现IRouteConstraint限制控制器名(转载)

自定义约束前 namespace MvcApplication2 { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); //默认 routes.MapRoute( name: "Default", url: "{controller}/

对ASP.NET MVC 的路由一点理解

这个东西,真搞不懂.看了网上的教程和文章,也不懂(也不清楚写那些文章的人自己是否真的懂).只好靠自己一顿乱摸索. 好比说,下面这个路由: //路由1 config.Routes.MapHttpRoute( name: "SysApi", routeTemplate: "api/SysManager/{action}/{id}", defaults: new { controller = "SysManager", id = RouteParame