• MVC特性路由的提供机制


    回顾:传统路由是如何提供的?

    我们知道最终匹配的路由数据是保存在RouteData中的,而RouteData通常又是封装在RequestContext中的,他们是在哪里被创建的呢?没错,回到了UrlRoutingModule,我们知道UrlRoutingModule通过注册HttpApplication的PostResolveRequestCache方法来分发IHttpHandler决定ASP.NET请求最终交给哪个IHttpHandler去处理的。其实在这之前,首先会通过当前请求的HttpContextBase解析虚拟路径,匹配路由。然而这个工作是RouteCollection完成的。

    public virtual void PostResolveRequestCache(HttpContextBase context)
    {
        RouteData routeData = this.RouteCollection.GetRouteData(context);
        if (routeData == null)
        {
            return;
        }
        //省略了注册IHttpHandler的代码
    }

    反编译查看Route的GetRouteData方法

    public RouteData GetRouteData(HttpContextBase httpContext)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException("httpContext");
        }
        if (httpContext.Request == null)
        {
            throw new ArgumentException(SR.GetString("RouteTable_ContextMissingRequest"), "httpContext");
        }
        if (base.Count == 0)
        {
            return null;
        }
        bool flag = false;
        bool flag2 = false;
        if (!this.RouteExistingFiles)
        {
            flag = this.IsRouteToExistingFile(httpContext);
            flag2 = true;
            if (flag)
            {
                return null;
            }
        }
        using (this.GetReadLock())
        {
            foreach (RouteBase current in this)
            {
                RouteData routeData = current.GetRouteData(httpContext);
                if (routeData != null)
                {
                    RouteData result;
                    if (!current.RouteExistingFiles)
                    {
                        if (!flag2)
                        {
                            flag = this.IsRouteToExistingFile(httpContext);
                        }
                        if (flag)
                        {
                            result = null;
                            return result;
                        }
                    }
                    result = routeData;
                    return result;
                }
            }
        }
        return null;
    }

    这个方法乍一看也太复杂了吧,再看一下还是复杂,花了点时间理清了,就是这样子的

    一句话解释:是文件都(RouteCollection和RouteBase)路由文件才成功匹配,不是文件只要路由规则匹配就成功匹配

    正题:特性路由的初始化

    特性路由是如何添加到路由系统中的呢?主要由AttributeRoutingMapper这个静态类实现的,先来跟随着源码一一道来。

    在程序启动的时候,MVC会通过DefaultControllerFactory找到所有的控制器类型,为每一个控制器内注册的特性路由创建一个或多个Route。

    public static void MapAttributeRoutes(RouteCollection routes, IInlineConstraintResolver constraintResolver)
    {
        DefaultControllerFactory typesLocator =
            DependencyResolver.Current.GetService<IControllerFactory>() as DefaultControllerFactory
            ?? ControllerBuilder.Current.GetControllerFactory() as DefaultControllerFactory
            ?? new DefaultControllerFactory();
        //获得所有的控制器类型
        IReadOnlyList<Type> controllerTypes = typesLocator.GetControllerTypes();
        //开始注册
        MapAttributeRoutes(routes, controllerTypes, constraintResolver);
    }

    寻找特新路由的过程中会把所有创建的Route都放在SubRouteCollection这个RouteEntry只读集合中(RouteEntry封装一个Route和一个string类型的Name),最后MVC会将SubRouteCollection封装成一个Route对象(实际上是RouteBase的子类RouteCollectionRoute),添加到RouteCollection中。

    public static void MapAttributeRoutes(RouteCollection routes, IEnumerable<Type> controllerTypes, IInlineConstraintResolver constraintResolver)
    {
        SubRouteCollection subRoutes = new SubRouteCollection();
      AddRouteEntries(subRoutes, controllerTypes, constraintResolver);
        IReadOnlyCollection<RouteEntry> entries = subRoutes.Entries;
        if (entries.Count > 0)
        {
        RouteCollectionRoute aggregrateRoute = new RouteCollectionRoute(subRoutes);
        routes.Add(aggregrateRoute);
        }
    }

    那她是如何根据控制器来创建Route的呢?先要认识几个类型,ControllerDescriptor(控制器描述器),可以根据它获取应用在控制器上的各种特性,类似的还有ActionDescriptor(方法描述器,注意区分MethodInfo),还有ActionMethodSelector(方法选择器),方法选择器有两个属性,分别是DirectRouteMethods表示应用了特性路由的方法,StandardRouteMethods表示普通的方法。

    具体处理每一个控制器类型的时候。会获取该控制器的方法选择器,对它的DirectRouteMethods和StandardRouteMethods分别处理。但两者有一个共同第一步,就是获取控制器前缀和Area前缀。控制器前缀直接从RoutePrefix特性中获取,Area前缀的话,会从RouteAreaAttribute获取,如果没有应用RouteAreaAttribute,那么就是控制器类型所在命名空间的最后一段。

    internal static void AddRouteEntries(SubRouteCollection collector, ReflectedAsyncControllerDescriptor controller, IInlineConstraintResolver constraintResolver)
    {
        string prefix = GetRoutePrefix(controller);
        RouteAreaAttribute area = controller.GetAreaFrom();
        string areaName = controller.GetAreaName(area);
        string areaPrefix = area != null ? area.AreaPrefix ?? area.AreaName : null;
        AsyncActionMethodSelector actionSelector = controller.Selector;
        //处理应用了特性路由的Action
        foreach (var method in actionSelector.DirectRouteMethods)
        {
            //...
        }
    
        //处理没有应用特性路由的Action
        foreach (var method in actionSelector.StandardRouteMethods)
        {
            //...
        }
    }

    处理DirectRouteMethods

    首先会根据该Action的方法描述器获取应用的所有特性路由,针对每一个特性路由创建一个RouteEntry,这个核心任务是由DirectRouteBuilder的Builder方法实现的。DirectRouteBuilder包含了所有和Route相关的属性,同时还包含一个ActionDescriptor数组,大多数属性我们都是熟悉的,有必要介绍一下_actions字段,这个字段保存的是这条特性路由是应用在哪些方法上的(对于应用了特性路由的Action来说只有一个),Precedence是模板根据约束生成的一个字段,用来排序。

    internal class DirectRouteBuilder : IDirectRouteBuilder
    {
        private readonly ActionDescriptor[] _actions;
        private readonly bool _targetIsAction;
        private string _template;
        public DirectRouteBuilder(IReadOnlyCollection<ActionDescriptor> actions, bool targetIsAction)
        {
            if (actions == null)
            {
                throw new ArgumentNullException("actions");
            }
            _actions = actions.ToArray();
            _targetIsAction = targetIsAction;
        }
        public string Name { get; set; }
    
        public string Template
        {
            get
            {
                return _template;
            }
            set
            {
                ParsedRoute = null;
                _template = value;
            }
        }
        public RouteValueDictionary Defaults { get; set; }
        public RouteValueDictionary Constraints { get; set; }
        public RouteValueDictionary DataTokens { get; set; }
        public int Order { get; set; }
        public decimal Precedence { get; set; }
        public IReadOnlyCollection<ActionDescriptor> Actions
        {
            get { return _actions; }
        }
    
        public bool TargetIsAction
        {
            get { return _targetIsAction; }
        }
    
        public virtual RouteEntry Build()
        {
            //省略
        }
    }

    对于应用了特性路由的Action来说。会找到所有应用在该Action上的特性路由,针对每一个特新路由创建相应的RouteEntry,而这一过程就是有RouteAttribute本身完成的。在这之前,会将特新路由的一些列属性封装到DirectRouteFactoryContext中。这里IDirectRouteFactory的唯一实现就是RouteAttribute。(不知道微软为什么不在这里直接写一个静态的Build方法,而要弄出DirectRouteBuilder的Builder和DirectRouteFactoryContext这两个类,个人觉得会简单很多)

    internal static RouteEntry CreateRouteEntry(string areaPrefix, string prefix, IDirectRouteFactory factory, IReadOnlyCollection<ActionDescriptor> actions, IInlineConstraintResolver constraintResolver,
     bool targetIsAction)
    {
        DirectRouteFactoryContext context = new DirectRouteFactoryContext(areaPrefix, prefix, actions, constraintResolver, targetIsAction);
        RouteEntry entry = factory.CreateRoute(context);
        return entry;
    }

    RouteAttribute的CreateRoute包含两本分,创建一个IDirectRouteBuilder,然后在调用他的Build方法。这里也表明了,我们可以通过RouteAttribute显示指定路由的优先级。

     RouteEntry IDirectRouteFactory.CreateRoute(DirectRouteFactoryContext context)
     {
         IDirectRouteBuilder builder = context.CreateBuilder(Template);
         builder.Name = Name;
         builder.Order = Order;
         return builder.Build();
     }

     CreateBuilder方法主要做了两件事,给路由添加控制器前缀,清除路由模板的内联约束。接下来看一下最核心的Build方法。

    (1)将actions(类型为ActionDescriptor)存入DataTokens的MS_DirectRouteActions键

    (2)如果定义路由的时候给定了Order,则将Order存入DataTokens的MS_DirectRouteOrder键

    (3)如果特性路由的模板和约束条件改变了precedence值,也将它写入DataTokens的MS_DirectRoutePrecedence键

    (4)如果actions对应的控制器描述器为同一个控制器类型,那就表示这条特性路由的控制器是有默认值的

    (5)如果目标是应用了特新路由的Action,并且actions只有一个,name这条特性路由的Action也是有默认值的,同时将true写入DataTokens的MS_DirectRouteTargetIsAction键

    (6)如果控制器使用的使用了RouteAreaAttribute特性,那么也将命名空间相关的值写入DataTokens中,UseNamespaceFallback用于以后控制器的查询。

    public virtual RouteEntry Build()
    {
        if (ParsedRoute == null)
        {
            ParsedRoute = RouteParser.Parse(Template);
        }
        RouteValueDictionary defaults;
        defaults = Copy(Defaults) ?? new RouteValueDictionary();
        RouteValueDictionary constraints = Copy(Constraints);
        RouteValueDictionary dataTokens = Copy(DataTokens) ?? new RouteValueDictionary();
        dataTokens[RouteDataTokenKeys.Actions] = _actions;
        ControllerDescriptor controllerDescriptor = GetControllerDescriptor();
        RouteAreaAttribute area = controllerDescriptor.GetAreaFrom();
        string areaName = controllerDescriptor.GetAreaName(area);
        if (areaName != null)
        {
            dataTokens[RouteDataTokenKeys.Area] = areaName;
            dataTokens[RouteDataTokenKeys.UseNamespaceFallback] = false;
            Type controllerType = controllerDescriptor.ControllerType;
            if (controllerType != null)
            {
                dataTokens[RouteDataTokenKeys.Namespaces] = new[] { controllerType.Namespace };
            }
        }
        Route route = new Route(Template, defaults, constraints, dataTokens, routeHandler: null);
        ConstraintValidation.Validate(route);
        return new RouteEntry(Name, route);
    }

    处理StandardRouteMethods

    对于没有应用特性路由的Action来说,和应用了特新路由的Action只有两点不一样,一个是RouteAttribute来说,他们的全都是Controller上的RouteAttribute提供的,还有就是方法描述器,是所有Action,为什么这样子呢?首先应用了特性路由的Action生成的Route会默认指定他的Action名称,由于现在的Action没有指定特性路由,全都是依靠Controller类型提供了,所以就不难理解了。

    特性路由的提供机制

    由于特性路由最终是以一个RouteCollectionRoute添加到RouteTable中的,所以他的重写了GetRouteData。这个类比较有意思,它本身是一个路由,但它却包括了一系列(特性)路由。

    (1)在集合中找到所有和当前虚拟路径匹配的路由

    (2)给RouteData的Values设置键MS_DirectRouteMatches,值为所有匹配的路由

    (3)取出匹配的路由的第一个,如果有controller默认值,就给RouteData的Values设置一个controller的值

    internal class RouteCollectionRoute : RouteBase, IReadOnlyCollection<RouteBase>
    {
        private readonly IReadOnlyCollection<RouteBase> _subRoutes;
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            List<RouteData> matches = new List<RouteData>();
            foreach (RouteBase route in _subRoutes)
            {
                var match = route.GetRouteData(httpContext);
                if (match != null)
                {
                    matches.Add(match);
                }
            }
            return CreateDirectRouteMatch(this, matches);
        }
        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            return null;
        }
      
        public static RouteData CreateDirectRouteMatch(RouteBase route, List<RouteData> matches)
        {
            if (matches.Count == 0)
            {
                return null;
            }
            else
            {
                var routeData = new RouteData();
                routeData.Route = route;
                routeData.RouteHandler = new MvcRouteHandler();
                ControllerDescriptor controllerDescriptor = matches[0].GetTargetControllerDescriptor();
                if (controllerDescriptor != null)
                {
                    routeData.Values[RouteDataTokenKeys.Controller] = controllerDescriptor.ControllerName;
                }
                return routeData;
            }
        }
    }

    可能也许有人会有疑问,特性路由最后也没有匹配一个具体的Route,事实上就是这样子的,RouteCollectionRoute就是一个具体的路由,在控制器的匹配工作上还要用到这里的知识,所以任重而道远哈,不过说回来这篇博客是我有史以来耗时最长的一个了。。鼓励一下自己!!!

  • 相关阅读:
    HR问“你目前有几个offer”,聪明人会怎么说?
    秋招还有 1 个月到达战场,请做好准备 !
    我人生中的第一场Java面试
    MZ头里面的东西。真他妈多
    特殊的一卦
    今天出门去办事,又倒霉了
    内核回调
    sys_call_table HOOK
    起一卦,看看情况
    我的简陋界面库的模块组成
  • 原文地址:https://www.cnblogs.com/cheesebar/p/6669090.html
Copyright © 2020-2023  润新知